WebGL 的灯光效果

888 阅读8分钟

真实世界里的灯光效果就是光线在物体表面进行反射和漫反射的结果,在 WebGL 里模拟这一效果需要知光源的位置、光线方向、光的强度和光线颜色等光源属性,同时还需要知道物体表面的方向、颜色和反射率等物体表面属性,将光源属性和物体表面属性进行相乘就能得到光照之后的效果了。

常见的光源有环境光、平行光、点光、聚光4种,常见的反射有漫反射和镜面反射,在复杂的三维引擎中,光线往往和材质相关。

一、光照模型

一个简单的光照模型就是光线照射到物体表面,会按照一定的入射角度漫反射出去,入射角越大,反射出去的光线越少。反射之后的光线就是物体最终的颜色。光线的反射可以用以下公式来表示:反射光颜色=入射光颜色×物体表面颜色×cosθ

根据光线入射方向物体表面方向可以计算光线入射角的值,在 WebGL 中光线入射方向可以用光线向量来表示,物体表面方向可以用法向量来表示,入射角的值可以使用向量点积来计算:dot(lightDirection,surfaceDirection)

法向量用来标识一个面的朝向,是一个单位向量,每个顶点都有法向量,顶点的法向量决定了三角面的法向量。

当模型位置应用模型矩阵改变后,法向量也会变化,可以用模型矩阵的逆转置矩阵来计算变化之后的法向量。

在一个相对完善的光照模型里,通常需要考虑物体自发光(emissive)、环境光(ambient)、漫反射(diffuse)和高光反射(specular)

这四个部分,这四部分光线是独立计算的,互相不会影响。将计算出来的4部分光线进行加法来叠加,就得到了最终的颜色。

二、光的类型

物体的最终颜色是多种光线反射影响之后颜色的叠加结果,计算颜色的反射融合用向量叉乘,计算颜色的叠加融合用向量加法

公式: 反射光颜色 = 入射光颜色 × 物体表面颜色 × cosθ

叠加光颜色 = 自发光颜色 + 环境光颜色 + 反射响颜色

2.1、自发光

自发光就是物体也充当光源的角色,能发出光线,会影响自身颜色和周围物体颜色。

2.2、环境光

环境光就可以理解为没有光源、没有方向的一种光,均匀地弥漫在三维场景中,可以近似成光线垂直与物体表面(cosθ=1)。通常设置一个全局的颜色值代表环境光,环境光的颜色和物体表面的颜色相乘就计算出两个光线融合之后的颜色了。

公式: 颜色 = 环境光颜色 x 物体表面颜色

let ambientLightColorLocation = gl.getUniformLocation(program, 'v_ambientLightColor');
gl.uniform3fv(ambientLightColorLocation, new Float32Array([0.2, 0.2, 0.2]));

let vertexShaderSource = `
    attribute vec4 a_position; // 模型局部坐标系下的顶点(这里的顶点数据已经是世界坐标系了)
    attribute vec3 a_color; // 模型各个面的颜色
    
    uniform mat4 u_mvpMatrix;
    uniform mat4 u_wMatrix;
    varying vec3 v_color;

    void main() {
      mat4 matrix = u_mvpMatrix * u_wMatrix;
      gl_Position = matrix * a_position;
      v_color = a_color;
    }
`;

let fragmentShaderSource = `
    precision mediump float;
    varying vec3 v_color;
    uniform vec3 v_ambientLightColor; // 环境光颜色

    void main() {
      // 环境光
      vec3 ambientColor = v_ambientLightColor * v_color;
      gl_FragColor = vec4(ambientColor, 1);
    }
`;

2.3、平行光

理想的平行光没有特定光源,可以理解为无限远处的一个无限大的平面,每一条光线的方向都是相同的,光线只能朝着一个方向,光强度不会随距离衰减。光照到物体表面全部是镜面反射。

平行光属于反射光,其公式: 反射光颜色 = 平行光颜色 × 物体表面颜色 × cosθ

平行光的光线方向直接输入,用一个归一化的向量来表示。

let normalAttributeLocation = gl.getAttribLocation(program, 'a_normal');
let lightDirectionLocation = gl.getUniformLocation(program, 'u_lightDirection');
let ambientLightColorLocation = gl.getUniformLocation(program, 'v_ambientLightColor');
setNormal(gl); // 类似顶点数据,绑定向量数据
gl.uniform3fv(lightDirectionLocation, M4.normalize([0.5, 0.7, 1])); // 归一化向量
gl.uniform3fv(ambientLightColorLocation, new Float32Array([0.2, 0.2, 0.2]));

// 着色器使用光线向量
let vertexShaderSource = `
    attribute vec4 a_position;
    attribute vec3 a_normal;	// 法向量
    varying vec3 v_normal;		// 光线向量

    void main() {
      gl_Position = a_position;
      v_normal = a_normal;
    }
`;
let fragmentShaderSource = `
    precision mediump float;
    varying vec3 v_normal; 					// 片元法向量
    uniform vec3 u_lightDirection;	// 光线方向
    uniform vec3 v_ambientLightColor; // 环境光颜色

    void main() {
      vec3 normal = normalize(v_normal);
      // 平行光
      float parallelLight = dot(normal, u_parallelLightDirection); // 计算平行光照
      vec3 parallel = parallelLight * v_color; // 将颜色rgb部分和平行光相乘
      vec3 ambientColor = v_ambientLightColor * v_color; // 环境光
      // 颜色合成
      gl_FragColor = vec4(parallel + ambientColor, 1);
    }
`;

2.4、点光

理想的点光其光源是一个点,光线是向四周发射的,每一条光线的方向都不同。

点光属于反射光,其公式: 反射光颜色 = 点光颜色 × 物体表面颜色 × cosθ

点光源的光线方向需要用光源位置顶点位置进行向量减法来求得。

在真实世界中,点光源会在物体表面形成一个“镜面高光”的亮斑,人眼和光线的发射的光线越接近亮斑就越明显。“镜面高光”亮斑效果需要计算每一个片元的视线向量和光线向量的1/2夹角处的halfVector向量,通过计算片元halfVector向量normalVector向量点的乘来判断二者的方向关系,如果二者方向相同则点乘结果为1,这个片元内的光线全部进入人眼,是亮斑的最亮点。

halfVector向量需要用光源线向量视线向量进行向量加法运算来求得。

上述计算结果是线性的,亮斑均匀过度到最边缘,但是真实世界的镜面高光的亮斑一般比较集中,为了实现这种效果,需要借助 glsl 的pow函数对高光进行处理,将“线性的高光范围”变为一个“指数高光范围”,这一亮斑就不在均匀分布,而是比较集中的分布在halfVector周围了。

let cameraPositionLocation = gl.getUniformLocation(program, 'u_cameraPosition');
let pointLightPositionLocation = gl.getUniformLocation(program, 'u_pointLightPosition');
let pointLightColorLocation = gl.getUniformLocation(program, 'u_pointLightColor');
let spotLightAngleLimitInLocation = gl.getUniformLocation(program, 'u_spotLightAngleLimitIn');
let spotLightAngleLimitOutLocation = gl.getUniformLocation(program, 'u_spotLightAngleLimitOut');
let spotLightDirectionLocation = gl.getUniformLocation(program, 'u_spotLightDirection');
var shininessLocation = gl.getUniformLocation(program, 'u_shininess');
let specularColorLocation = gl.getUniformLocation(program, 'u_specularColor');
gl.uniform3fv(cameraPositionLocation, [100, 150, 200]);
gl.uniform3fv(pointLightPositionLocation, [20, 30, 60]);
gl.uniform3fv(pointLightColorLocation, [1, 0.6, 0.6]);
gl.uniform1f(spotLightAngleLimitInLocation, Math.cos((10 * Math.PI) / 180));
gl.uniform1f(spotLightAngleLimitOutLocation, Math.cos((20 * Math.PI) / 180));
gl.uniform3fv(spotLightDirectionLocation, [-vMatrix[8], -vMatrix[9], -vMatrix[10]]); // vMatrix是视图矩阵
gl.uniform1f(shininessLocation, 35);
gl.uniform3fv(specularColorLocation, [1, 0.6, 0.6]);

let vertexShaderSource = `
    attribute vec4 a_position; // 模型局部坐标系下的顶点(这里的顶点数据已经是世界坐标系了)
    attribute vec3 a_normal; // 模型局部坐标系下的法向量(这里的法向量数据已经是世界坐标系了)
    
    uniform mat4 u_mvpMatrix;
    uniform mat4 u_mMatrix;
    uniform mat4 u_wMatrix;
    uniform mat4 u_nMatrix;
    uniform vec3 u_pointLightPosition;
    uniform vec3 u_cameraPosition;

    varying vec3 v_normal; // 世界坐标下法向量,用于和平行光相乘
    varying vec3 v_surfaceToLight; // 世界坐标下点光源方向,用于计算光照
    varying vec3 v_surfaceToCamera; // 世界坐标下点视线方向,用于计算光照
    varying vec3 v_color;

    void main() {
      mat4 matrix = u_mvpMatrix * u_wMatrix;
      gl_Position = matrix * a_position;

      v_normal = mat3(u_nMatrix) * a_normal; // 重新计算世界坐标的下法向量
      vec3 surfacePosition = (u_wMatrix * a_position).xyz; // 变换之后的顶点的世界坐标
      v_surfaceToLight = u_pointLightPosition - surfacePosition; // 计算世界坐标下每个顶点对应的点光源方向
      v_surfaceToCamera = u_cameraPosition - surfacePosition; // 计算世界坐标下每个顶点对应的视线方向
    }
`;

let fragmentShaderSource = `
    precision mediump float;
    varying vec3 v_color;
    varying vec3 v_normal; // 片元法向量
    varying vec3 v_surfaceToCamera; // 视线方向
    varying vec3 v_surfaceToLight; // 点光源光线方向

    uniform vec3 u_pointLightColor; // 点光颜色
    uniform vec3 u_specularColor; // 高光颜色
    uniform float u_shininess; // 高光颜强度

    void main() {
      vec3 normal = normalize(v_normal);
      // 点光
      vec3 surfaceToLightDirection = normalize(v_surfaceToLight); // 点光方向
      vec3 surfaceToCameraDirection = normalize(v_surfaceToCamera); // 视线方向
      vec3 halfVector = normalize(surfaceToLightDirection + surfaceToCameraDirection); // 视线和点光方向夹角的一半
      float pointLight = dot(normal, surfaceToLightDirection); // 计算点光照
      float specular = 0.0;
      if (pointLight > 0.0) {
        specular = pow(dot(normal, halfVector), u_shininess); // 高光强度(值越大亮斑越小)
      }
      // 颜色合成
      vec3 spotColor = pointLight * u_pointLightColor * v_color; // 将颜色rgb部分和点光、点光颜色相乘
      vec3 specularColor = specular * u_specularColor; // 加上高光强度和颜色
      vec3 ambientColor = v_ambientLightColor * v_color; // 环境光
      gl_FragColor = vec4(spotColor + specularColor + ambientColor, 1);
    }
`;

2.5、聚光

理想的聚光其光源是一个点,光线是一个锥形光束,每一条光线的方向都不同,光强度会随半径衰减。

聚光属于反射光,其公式: 反射光颜色 = 聚光颜色 × 物体表面颜色 × cosθ

衰减范围可以通过设定一个内圈半径和外圈半径,在这个环内差值0~1来实现。利用了 glsl 的smoothstep函数。

  let vertexShaderSource = `
      attribute vec4 a_position; // 模型局部坐标系下的顶点(这里的顶点数据已经是世界坐标系了)
      attribute vec3 a_color; // 模型各个面的颜色
      attribute vec3 a_normal; // 模型局部坐标系下的法向量(这里的法向量数据已经是世界坐标系了)
      
      uniform mat4 u_mvpMatrix;
      uniform mat4 u_mMatrix;
      uniform mat4 u_wMatrix;
      uniform mat4 u_nMatrix;
      uniform vec3 u_pointLightPosition;
      uniform vec3 u_cameraPosition;

      varying vec3 v_normal; // 世界坐标下法向量,用于和平行光相乘
      varying vec3 v_surfaceToLight; // 世界坐标下点光源方向,用于计算光照
      varying vec3 v_surfaceToCamera; // 世界坐标下点视线方向,用于计算光照
      varying vec3 v_color;

      void main() {
        mat4 matrix = u_mvpMatrix * u_wMatrix;
        gl_Position = matrix * a_position;

        v_normal = mat3(u_nMatrix) * a_normal; // 重新计算世界坐标的下法向量
        vec3 surfacePosition = (u_wMatrix * a_position).xyz; // 变换之后的顶点的世界坐标
        v_surfaceToLight = u_pointLightPosition - surfacePosition; // 计算世界坐标下每个顶点对应的点光源方向
        v_surfaceToCamera = u_cameraPosition - surfacePosition; // 计算世界坐标下每个顶点对应的视线方向

        v_color = a_color;
      }
	`;

  let fragmentShaderSource = `
      precision mediump float;
      varying vec3 v_color;
      varying vec3 v_normal; // 片元法向量
      varying vec3 v_surfaceToCamera; // 视线方向
      varying vec3 v_surfaceToLight; // 点光源光线方向

      uniform vec3 u_parallelLightDirection; // 平行光的方向,传进来的就是归一化向量
      uniform float u_spotLightAngleLimitIn; // 点光的范围(聚光大小)
      uniform float u_spotLightAngleLimitOut; // 点光的范围(聚光大小)
      uniform vec3 u_spotLightDirection; // 聚光方向 
      uniform vec3 v_ambientLightColor; // 环境光颜色
      uniform vec3 u_pointLightColor; // 点光颜色
      uniform vec3 u_specularColor; // 高光颜色
      uniform float u_shininess; // 高光颜强度

      void main() {
        vec3 normal = normalize(v_normal);
        // 聚光
        vec3 surfaceToLightDirection = normalize(v_surfaceToLight); // 点光方向
        vec3 surfaceToCameraDirection = normalize(v_surfaceToCamera); // 视线方向
        vec3 halfVector = normalize(surfaceToLightDirection + surfaceToCameraDirection); // 视线和点光方向夹角的一半
        float dotFromDirection = dot(surfaceToLightDirection, -u_spotLightDirection);
        float inLight = smoothstep(u_spotLightAngleLimitOut, u_spotLightAngleLimitIn, dotFromDirection);
        float spotLight = inLight * dot(normal, surfaceToLightDirection);
        float specular = inLight * pow(dot(normal, halfVector), u_shininess);    
        // 颜色合成    
        vec3 spotColor = spotLight * v_color; // 将颜色rgb部分和聚光相乘
        vec3 specularColor = specular * u_specularColor; // 加上高光强度和颜色
        vec3 ambientColor = v_ambientLightColor * v_color; // 环境光
        gl_FragColor = vec4(spotColor + specularColor + ambientColor, 1);
      }
  `;



这是WebGL 系列的入门文章,免费订阅,如有帮助请点赞收藏,纰漏之处欢迎指正!

qrcode_for_gh_3695c3ae18f4_258.jpg

也欢迎关注公众号交流知识哇😄