Shader之所以叫着色器,是因为它有shading着色这个概念。

shading着色,指的是在三维模型,甚至是插画中通过不同的亮度表现深度的手法,这种方法可以使得3D场景显得更加逼真和生动。

所以说,我们要怎么做,才可以计算得到物体表面的亮度呢?图形学,给了我们答案,那就是用光照模型

所以本章节的主题就是在Shader实现基本的光照模型

开始模板

首先我们来建立一个新的 HTML 文件吧:

<style>
  body {
    margin: 0;
    background: black;
  }
</style>
<div id="sketch"></div>
<script src="https://unpkg.com/kokomi.js@1.9.78/build/kokomi.umd.js"></script>
<script src="https://unpkg.com/three@0.154.0/build/three.min.js"></script>
<script>
  class Sketch extends kokomi.Base {
    create() {
      this.camera.position.set(0, 0, 5);
      new kokomi.OrbitControls(this);

      const geometry = new THREE.SphereGeometry(2, 64, 64);
      // const geometry = new THREE.PlaneGeometry(4, 4);
      const material = new THREE.ShaderMaterial({
        vertexShader: /* glsl */ `
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;

varying vec2 vUv;

void main(){
    vec3 p=position;
    gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.);
    
    vUv=uv;
}
  `,
        fragmentShader: /* glsl */ `
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;

varying vec2 vUv;

void main(){
    vec2 uv=vUv;
    gl_FragColor=vec4(uv,0.,1.);
}
  `,
      });
      const mesh = new THREE.Mesh(geometry, material);
      this.scene.add(mesh);

      const uj = new kokomi.UniformInjector(this);
      material.uniforms = {
        ...material.uniforms,
        ...uj.shadertoyUniforms,
      };
      this.update(() => {
        uj.injectShadertoyUniforms(material.uniforms);
      });
    }
  }

  const sketch = new Sketch("#sketch");
  sketch.create();
</script>

然后我们以此为基础开始光照模型的学习。

冯氏光照模型

20 世纪中后期,一位叫做裴祥风的图形学学者提出了“冯氏光照模型”的理念,后来得到了 CG 以及图形学界的广泛应用。

这个模型主要由三种光照组成:环境光漫反射镜面反射

让我们关注片元着色器,在main中定义好几个变量,结果颜色col设置为黑色(因为还没有光照),物体的基础颜色objectColor设置为白色,光照颜色lightColor为亮红色。

void main(){
    vec2 uv=vUv;

    vec3 col = vec3(0.);
    vec3 objectColor = vec3(1.);
    vec3 lightColor = vec3(.875, .286, .333);
    gl_FragColor=vec4(col,1.);
}

环境光

指的是物体表面收到来自周围环境光源的光照,这种光照均匀的分布在物体的表面上。

实现起来也是非常的简单,我们只需要给出光的颜色,然后乘上光的强度就可以得出结果了。

    // 光照强度0.2
    float ambIntensity = .2;
    // 光照颜色 * 强度 = 环境光
    vec3 ambient = lightColor * ambIntensity;
    // 输出颜色 加上 光照颜色 乘以 物体颜色就是结果了
    col += ambient*objectColor;
    gl_FragColor=vec4(col,1.);

可以看出来,物体整体是偏暗的,因为环境光的作用显示出了淡淡的暗红色。

漫反射

漫反射说的是光照照射到物体表面之后,光线被物体表面反射,反射光线会向所有方向散射的现象。

大概手绘了一个示意图:

也就是说,当光线照射到物体表面的时候,会和法向量N形成一个夹角θ

所谓的法向量就是与物体垂直的向量。

θ这个夹角就叫做入射角。入射角越大,就说明光照强度越小;反之,如果入射角越小,就说明光照强度越大;当入射角为 0 时,光线和法向量方向会重叠,此时光照垂直照射物体的时候,强度是最大的。

要获得漫反射的光,需要的步骤有如下:

  1. 获取法向量N
  2. 获取光线的方向向量
  3. 计算漫反射因子。
  4. 计算漫反射光。

那我们一步步来处理吧:

获取法向量

先说法向量N,这个变量其实在顶点着色器里已经有了,叫normal,把它传递给片元着色器就要用到varying变量。

在顶点着色器上声明normal

varying vec3 vNormal;

在主函数main中,将normal赋值给vNormal

void main() {
    ...
    vNormal = normal;
}

在片元着色器的上方也声明一下:

varying vec3 vNormal;

如此以来,我们就成功的获取了法向量N

获取光线的方向向量

在计算光线的方向向量之前,我们要算一下光线照射到物体的那一部分片元的位置,而这里的位置是世界空间坐标系的,也就是从物体坐标系通过模型矩阵变换之后得出的结果。

咱们在顶点着色器的顶部,声明varying变量vWorldPosition,表示的是片元的位置,然后接下来在main函数中计算它:

varying vec3 vWorldPosition;
...
// vWorldPosition=vec3(modelMatrix*vec4(p,1.));
    vWorldPosition=(modelMatrix*vec4(p,1.)).xyz;

这里有两种写法,xyz 打点调用和 vec3 转换都是可以的,看个人习惯了!

随后在片元着色器上也声明一下:

varying vec3 vWorldPosition;

如此一来,我们就在片元着色器上获取了片段(光线照射到物体的那一部分片元)的位置,接下来就可以计算光线的方向向量:

    vec3 lightPos = vec3(0., 0., 5.);
    vec3 lightDir = normalize(lightPos - vWorldPosition);

来解释一下这些变量都是啥:

  • lightPos:光照位置,这里我随便设置了一下
  • lightDir:光线方向向量,其值就是光照位置和片元位置也就是片段的差,我们需要用normalize来归一化一下结果。

计算漫反射因子

漫反射因子就是漫反射强度,这个强度跟入射角θ有关系,并且他们是呈现反比例的关系的,既然是反比例关系,那我们完全可以用入射角的余弦值来作为漫反射因子。在数学领域,求两个向量的点积就可以得到夹角的余弦值。在GLSL中有一个方法叫做dot,它就可以直接计算两个数的点积,我们用它来计算法向量与光线方向向量的余弦值。

float diff = dot(vNormal, lightDir);

但是,余弦函数在角度>90 度的时候会出现负数的情况,为了避免出现负数,我们可以用max函数来保证最后的结果不小于 0。
所以就是:

float diff = max(dot(vNormal, lightDir), 0.);

计算漫反射光

好了,我们得到了法向量光线方向向量漫反射因子,集齐了三个必要条件,可以开始计算漫反射光了。

vec3 diffuse=lightColor*diff;
col+=diffuse*objectColor;

我们只需要把光照颜色和漫反射因子相乘,就可以得到漫反射光照,再把漫反射光照和物体的颜色相乘,加到输出颜色上,就能得到被光照影响之后的物体。

球体上产生了明显的明暗区分,这就是漫反射光产生的效果了。

镜面反射

镜面反射指的是光源照射到物体表面之后,光线被物体表面反射,反射光线呈现出明亮的高光点的现象

我画了一个差不多的原理图:

就跟漫反射相似,镜面反射也是跟法向量和光照方向有关系的,只不过多了一个眼睛的方向。与此同时,镜面反射也取决于物体自身的属性,如果是镜子,那它的表面就很光滑,可以让光线同时往一个方向聚焦。

要获得镜面反射的光,同样需要以下的步骤:

  1. 获取反射光的方向向量
  2. 获取眼睛的方向向量
  3. 计算镜面高光因子
  4. 计算镜面反射光

让我们开始吧:

反射光的方向向量

GLSL中,reflect函数将算出反射光的向量,接受两个参数:入射光方向 & 法向量

vec3 reflectDir = reflect(-lightDir, vNormal);

第一个参数要求的方向是光照指向片元,我们之前算出来的那个光照方向lightDir是片元指向光照的,所以加个负号给翻过来就好。

眼睛的方向向量

眼睛的方向取名为viewDir,等于眼睛位置cameraPosition减去片元的位置vWorldPosition,结果也需要归一化。

vec3 viewDir = normalize(cameraPosition-vWorldPosition);

镜面高光因子

跟之前计算漫反射因子差不多,要计算反射光的方向向量 & 眼睛的方向向量的点积,同时要确保值不能是负数。

float spec = max(dot(viewDir, reflectDir), 0.);

然而,这样是不够的,因为上面提到反射还跟物体本身的属性有关系,所以考虑到这一点,我们引入一个额外的变量——“反光度”,我们取名叫shininess,反光度越大,光线就越是聚焦,这里把反光度设置为 30 试试。

float shininess = 30.;
spec = pow(max(dot(viewDir, reflectDir),0.), shininess);

计算镜面反射光

这下要素集齐了,要召唤最终效果了!我们只要把光照颜色与镜面高光因子相乘,就可以得到镜面反射的最终光照了,光照和物体相乘,加上最终的颜色输出,就是我们的被光照影响之后的物体的最后样貌啦!

vec3 specular = lightColor * spec;
col += specular * objectColor;

真是让人激动,一个比较成功的 3D 模型就这样手搓出来了!!

文章末尾贴上全部代码:

<style>
  body {
    margin: 0;
    background: black;
  }
</style>
<div id="sketch"></div>
<script src="https://unpkg.com/kokomi.js@1.9.78/build/kokomi.umd.js"></script>
<script src="https://unpkg.com/three@0.154.0/build/three.min.js"></script>
<script>
  class Sketch extends kokomi.Base {
    create() {
      this.camera.position.set(0, 0, 5);
      new kokomi.OrbitControls(this);

      const geometry = new THREE.SphereGeometry(2, 64, 64);
      const material = new THREE.ShaderMaterial({
        vertexShader: /* glsl */ `
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;

varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vWorldPosition;

void main(){
  vec3 p=position;
  gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.);
  
  vUv=uv;
  vNormal=normal;
  vWorldPosition=(modelMatrix*vec4(p,1)).xyz;
}
`,
        fragmentShader: /* glsl */ `
uniform float iTime;
uniform vec3 iResolution;
uniform vec4 iMouse;

varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vWorldPosition;

void main(){
  vec2 uv=vUv;
  
  vec3 col=vec3(0.);
  vec3 objectColor=vec3(1.);
  vec3 lightColor=vec3(.875,.286,.333);
  
  // 环境光
  float ambIntensity=.2;
  vec3 ambient=lightColor*ambIntensity;
  col+=ambient*objectColor;
  // 漫反射
  vec3 lightPos=vec3(0., 0., 5.);
  vec3 lightDir=normalize(lightPos-vWorldPosition);
  float diff=dot(vNormal,lightDir);
  diff=max(diff,0.);
  vec3 diffuse=lightColor*diff;
  col+=diffuse*objectColor;
  // 镜面反射
  vec3 reflectDir=reflect(-lightDir,vNormal);
  vec3 viewDir=normalize(cameraPosition-vWorldPosition);
  float spec=dot(viewDir,reflectDir);
  spec=max(spec,0.);
  float shininess=32.;
  spec=pow(spec,shininess);
  vec3 specular=lightColor*spec;
  col+=specular*objectColor;
  
  gl_FragColor=vec4(col,1.);
}
`,
      });
      const mesh = new THREE.Mesh(geometry, material);
      this.scene.add(mesh);

      const uj = new kokomi.UniformInjector(this);
      material.uniforms = {
        ...material.uniforms,
        ...uj.shadertoyUniforms,
      };
      this.update(() => {
        uj.injectShadertoyUniforms(material.uniforms);
      });
    }
  }

  const sketch = new Sketch("#sketch");
  sketch.create();
</script>
最后修改:2024 年 08 月 14 日
收款不要了,给孩子补充点点赞数吧