WebGL第三十二课:简单2D光照

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

本文标题:WebGL第三十二课:简单2D光照
复制代码

友情提示

这篇文章是WebGL课程专栏的第32篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。

本课代码直接跳转获取:三十二课代码

引子

尽量将一些基础的知识在2D部分讲完,因为这些概念是2D和3D通用的,比如说,光照的概念。

给渲染的图形加一点光照可以让效果增加质感。这也是现代图形学的主要问题之一,就是尽量让光线更真实。

当然,我们这里就用一般的简化的算法就够了。

比如下面这个图,就好像一个探照灯一样,照到哪里,哪里就亮,并且有柔和的发散效果:

image.png

image.png

本篇就来讲一下,上面的效果如何实现。

回顾一下,WebGL 如何显示图片

    1. 将图片传到gl中去
    1. 设置顶点的uv
    1. 在fragment_shader中进行采样

这里重点回顾一下第二点,就是顶点uv的设定。

还是要先讲一下,我们的顶点数据是什么样子的:

        this.data = [
            // 第一个三角形
            -1, -1, 0, 0, // 左下角点
            1, -1, 1, 0, // 右下角点
            1, 1, 1, 1, // 右上角点
            // 第二个三角形
            1, 1, 1, 1,  // 右上角点
            -1, 1, 0, 1,  // 左上角点
            -1, -1, 0, 0,  // 左下角点
        ];

复制代码

我们可以看见是由两个三角形组成的,一共六个点。
每个顶点包含四个数据,分别是

  • x
  • y
  • u
  • v

例如,第一个左下角点:

            -1, -1, 0, 0, // 左下角点
复制代码

(-1,-1)代表这个点是屏幕左下角的意思。
(0,0)就是说这个点应该和图片的左下角锚定的意思。

然后依次将这些点的坐标和uv写出来,就是上述代码。

有人问了,为什么是六个点,不是四个点。其中有些点的数据不是重复了吗?

问得好,因为我们绘制用的模式就是这样的,一定要这样列出来,然后按照一个三角形一个三角形这样的画,才行。

当然,画三角形有其他模式,我们这里为了讲解,就采用这种绘制模式,无伤大雅。

拉取图片和传递到gl

这个前面关于贴图的文章已经说过了,详情请看二十六课。

绘制三角形的过程

这个过程十分重要,因为对于我们理解贴图已经光照都有帮助。

首先,第一个三角形,就是上面data代码的前三行,也就是前三个点。

绘制的时候,先确定位置,这个时候可以定出这个三角形就是右下角的半个三角形

三个点的位置定了,那么这三个点的颜色,可以根据这三个点的uv信息,去图片里采样,如果锚定没问题的话,三个点最后显示的样子如下:

image.png

问题就来了,三个点的uv当然可以在图片中采样到正确的颜色,那么这三个点中间的颜色,又是从哪里来的?

答案就是插值:

fragment_shader会对中间区域进行插值,然后求出uv,然后采样。

这一点很重要,因为一会算光照的时候有用。

光照怎么算

先用一个很简单的算法搞定:

设定一个uniform变量
uniform vec2 u_lightPos

这个变量用来代表光源的位置,我们的算法很简单,

l i g h t = 1 d i s t a n c e ( p o s , u _ l i g h t P o s ) light = 1 - distance(pos, u\_lightPos)

也就是说,离这个光源越近,就越亮,越远就越暗,距离超过1,直接就是黑暗一片。

实现在shader中

第一个问题就是,上面的代码应该写到 vertex_shader 还是 fragment_shader

不妨做做实验,我们先写在vertex_shader中:

vertex_shader:

    <script id="vertex_shader" type="myshader">
        // Vertex Shader
        precision mediump int;
        precision mediump float;
        
        uniform mat3 u_all;
        uniform vec2 u_lightPos;         // 光源的位置

        attribute vec2 a_PointVertex; // 顶点坐标
        attribute vec2 a_PointUV; // 顶点UV

        varying vec2 uv;
        varying float light;

        void main() {
          vec3 coord = u_all * vec3(a_PointVertex, 1.0);
          gl_Position = vec4(coord.x, coord.y, 0.0, 1.0);
          uv = a_PointUV;
          uv.y = 1.0 - uv.y;
          light = 1.0 - distance(coord.xy, u_lightPos);
        }
    </script>

复制代码

fragment_shader:

    <script id="fragment_shader" type="myshader">
        // Fragment shader
        precision mediump int;
        precision mediump float;

        uniform sampler2D u_funny_cat; // 有趣的猫的图片
        varying vec2 uv;
        varying float light;

        void main() {
            vec4 sample_color = texture2D(u_funny_cat, uv);
            gl_FragColor = vec4(sample_color.xyz * light, 1.0);
        }
    </script>

复制代码

结果一运行,如图:

image.png

全黑,这是为什么呢?

我们如果没有设置uniform变量 的话, 这个变量默认就是0,也就是说,每一个维度都是0。

就是说,默认光源的位置就是 ( 0 , 0 ) (0,0)

那么我data中的六个点的坐标离 ( 0 , 0 ) (0,0) 点的距离都是 2 \sqrt 2

那么 这六个点 算到的 l i g h t = ( 1 2 ) < 0 light = (1-\sqrt 2)<0

问题就来了,四周的六个点,算到的是 ( 1 2 ) (1-\sqrt 2) ,为什么中间也黑了?

对于每一个三角形来说,light 计算好之后,通过 varying的方式,传递给fragment_shaderfragment_shader收到了同样的三个light值,那么他不管怎么插值,中间的部分,也只能是 ( 1 2 ) (1-\sqrt 2) 了。

也就是说,这个过程,应该放到fragment_shader中,来计算。

我们把顶点的位置通过varying的方式,传递给fragment_shader,然后根据公式来计算,:

    <script id="vertex_shader" type="myshader">
        // Vertex Shader
        precision mediump int;
        precision mediump float;
        
        uniform mat3 u_all;
        attribute vec2 a_PointVertex; // 顶点坐标
        attribute vec2 a_PointUV; // 顶点坐标

        varying vec2 uv;
        varying vec2 pos;

        void main() {
          vec3 coord = u_all * vec3(a_PointVertex, 1.0);
          gl_Position = vec4(coord.x, coord.y, 0.0, 1.0);
          uv = a_PointUV;
          uv.y = 1.0 - uv.y;
          pos = coord.xy;
        }
    </script>
    <script id="fragment_shader" type="myshader">
        // Fragment shader
        precision mediump int;
        precision mediump float;

        uniform sampler2D u_funny_cat; // 有趣的猫的图片
        uniform vec2 u_lightPos;         // 光源的位置

        varying vec2 uv;
        varying vec2 pos;

        void main() {
            vec4 sample_color = texture2D(u_funny_cat, uv);
            float light = 1.0 - distance(pos, u_lightPos);
            gl_FragColor = vec4(sample_color.xyz * light, 1.0);
        }
    </script>

复制代码

这样的出的结果就如下了:

image.png

搞点气氛

一束白光打过去,没啥意思,我们整点气氛:

image.png

那这个怎么搞的,简单。

我们将图片上采样下来的结果, G 分量和 B分量,全部搞成0。然后只保留R分量就行了。

代码如下:


        void main() {
            vec4 sample_color = texture2D(u_funny_cat, uv);
            float light = 1.0 - distance(pos, u_lightPos);
            sample_color.x  = sample_color.x * light;
            sample_color.y  = 0.0;
            sample_color.z  = 0.0;
            
            gl_FragColor = vec4(sample_color.xyz, 1.0);
        }

复制代码

动起来

依然在console里写个定时器,让光源的位置动一动,让结果更有意思:

// 在浏览器直接敲:
    setInterval(() => {
        let x = (Math.random() - 0.5) * 2;
        let y = (Math.random() - 0.5) * 2;
        gl.uniform2f(u_lightPos, x, y);
        gl_draw();
    }, 50);
复制代码

效果如下:

2021-11-08_2281841392291411363.31.15.gif
---
---
  正文结束,下面是答疑
复制代码

小能能说:上面讲的插值那里,还真需要想一想呢。。

  • 这个过程需要掌握,自然很多问题就有了答案。

猜你喜欢

转载自juejin.im/post/7028101773629849613