点光源与自发光

大家好,我是西瓜,你现在看到的是 2021 iThome 铁人赛『如何在网页中绘制 3D 场景?从 WebGL 的基础开始说起』系列文章的第 20 篇文章。本系列文章从 WebGL 基本运作机制以及使用的原理开始介绍,最後建构出绘制 3D、光影效果之网页。绘制出简易的 3D 场景後,本章节加入光照效果使得成像更加真实,如果在阅读本文时觉得有什麽未知的东西被当成已知的,可能可以在前面的文章中找到相关的内容

从无限远照射场景的平行光适合用来模拟太阳这类型的光源,如果是室内的灯泡光源呢?本篇将在场景中加入一个黄色自发光灯泡,并把平行光改成以这颗灯泡作为点光源

输入光源位置并计算光线方向

在平行光的环境下,所有位置的光线方向都一样,因此只需要一个 uniform u_lightDir 便可以,但是在点光源的情况下会因为顶点/表面位置不同而有不同的光线方向,而光线方向可以透过 vertex shader 计算,并利用平滑补间使得 fragment shader 得到对应表面所街收到的光线方向,因此在 vertex shader 中使用 uniform 接收光源位置 u_worldLightPosition,并且计算出光线方向使用 varying v_surfaceToLight 传给 fragment shader 使用:

 attribute vec4 a_position;
 attribute vec2 a_texcoord;
 attribute vec4 a_normal;

 uniform mat4 u_matrix;
 uniform mat4 u_worldMatrix;
 uniform mat4 u_normalMatrix;
 uniform vec3 u_worldViewerPosition;
+uniform vec3 u_worldLightPosition;
  
 varying vec2 v_texcoord;
 varying vec3 v_normal;
 varying vec3 v_surfaceToViewer;
+varying vec3 v_surfaceToLight;

 void main() {
   gl_Position = u_matrix * a_position;
   v_texcoord = vec2(a_texcoord.x, 1.0 - a_texcoord.y);
   v_normal = (u_normalMatrix * a_normal).xyz;
   vec3 worldPosition = (u_worldMatrix * a_position).xyz;
   v_surfaceToViewer = u_worldViewerPosition - worldPosition;
+  v_surfaceToLight = u_worldLightPosition - worldPosition;
 }

在 fragment shader 上做的事情不会很困难,就只是从 u_lightDir 改用 v_surfaceToLight

 precision highp float;

 uniform vec3 u_diffuse;
 uniform sampler2D u_texture;
-uniform vec3 u_lightDir;
 uniform vec3 u_specular;
 uniform float u_specularExponent;
  
 varying vec2 v_texcoord;
 varying vec3 v_normal;
 varying vec3 v_surfaceToViewer;
+varying vec3 v_surfaceToLight;
  
 void main() {
   vec3 diffuse = u_diffuse + texture2D(u_texture, v_texcoord).rgb; 
   vec3 normal = normalize(v_normal);
-  vec3 surfaceToLightDir = normalize(-u_lightDir);
-  float diffuseBrightness = clamp(dot(surfaceToLightDir, normal), 0.0, 1.0);
+  vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
+  float diffuseBrightness = clamp(dot(surfaceToLightDirection, normal), 0.0, 1.0);
  
   vec3 surfaceToViewerDirection = normalize(v_surfaceToViewer);
-  vec3 halfVector = normalize(surfaceToLightDir + surfaceToViewerDirection);
+  vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewerDirection);
   float specularBrightness = clamp(pow(dot(halfVector, normal), u_specularExponent), 0.0, 1.0);

   gl_FragColor = vec4(
     diffuse * diffuseBrightness +
     u_specular * specularBrightness,
     1
   );
 }

笔者设定光源的初始位置在 [0, 2, 0],并且设定该设定的 uniform:

 async function setup() {
   // ...
   return {
     // ...
     state: {
-      lightDir: [0, -1, 0],
+      lightPosition: [0, 2, 0],
     }
   }
 }

 function render(app) {
   twgl.setUniforms(programInfo, {
     u_worldViewerPosition: state.cameraPosition,
-    u_lightDir: state.lightDir,
+    u_worldLightPosition: state.lightPosition,
     u_specular: [1, 1, 1],
 });

同时也将从 DOM 进行的使用者控制部份调整好,程序码比较琐碎笔者就不列了,改完之後可以调整光源的 y 轴位置,观察接近地面时反射光的表现:

light-position-demo

加入灯泡表示点光源位置

我们就用小球来表示点光源的位置,可以重复使用现有的 objects.ball 物件,因此不需要修改 setup(),直接在 render() 多渲染一次 objects.ball,并利用 worldMatrix 使得物件缩小且平移至光源位置:

function render(app) {
  // ...
  { // light bulb
    gl.bindVertexArray(objects.ball.vao);

    const worldMatrix = matrix4.multiply(
      matrix4.translate(...state.lightPosition),
      matrix4.scale(0.1, 0.1, 0.1),
    );

    twgl.setUniforms(programInfo, {
      u_matrix: matrix4.multiply(viewMatrix, worldMatrix),
      u_worldMatrix: worldMatrix,
      u_normalMatrix: matrix4.transpose(matrix4.inverse(worldMatrix)),
      u_diffuse: [1, 1, 1],
      u_texture: textures.nil,
      u_specularExponent: 1000,
    });

    twgl.drawBufferInfo(gl, objects.ball.bufferInfo);
  }
  // ...
}

虽然笔者把灯泡的 u_diffuse 给上 [1, 1, 1],但是因为光源在球体内部,因此灯泡球体呈现黑色:

with-black-bulb

Emissive 自发光

为了让灯泡球体有颜色,我们可以在 fragment shader 中加上一个 uniform,计算 gl_FragColor 时直接加上这个颜色,这个颜色即为自发光,变数名称命名为 u_emissive:

 precision highp float;

 uniform sampler2D u_texture;
 uniform vec3 u_specular;
 uniform float u_specularExponent;
+uniform vec3 u_emissive;

 // ...

 void main() {
   // ...

   gl_FragColor = vec4(
     diffuse * diffuseBrightness +
-    u_specular * specularBrightness,
+    u_specular * specularBrightness +
+    u_emissive,
     1
   );
 }

接下来对各个物件指定自发光颜色,笔者让原本的球体也有一点点的亮度 [0.15, 0.15, 0.15],而原本的灯泡就给黄色 [1, 1, 0]:

 function render(app) {
   // ...
   
   { // ball
     twgl.setUniforms(programInfo, {
       // ...
+      u_emissive: [0.15, 0.15, 0.15],
     });
   }
   
   { // light bulb
     twgl.setUniforms(programInfo, {
       // ...
+      u_emissive: [1, 1, 0],
     });
   }

   { // ground
     twgl.setUniforms(programInfo, {
       // ...
+      u_emissive: [0, 0, 0],
     });
   }
 }

今天的目标就完成啦:

yellow-bulb-point-lighting

事实上,物件『材质』对於光的反应、产生的颜色很可能远不只这个系列文所提到的散射光、反射光、自发光,以这篇读取 .obj/.mtl 3D 模型材质资料的文章来看,至少就还有环境光(ambient)等等

本篇完整的程序码可以在这边找到:


<<:  [2021铁人赛 Day05] General Skills 02

>>:  [Day5] Process

Spring Framework X Kotlin Day 11 NoSQL

GitHub Repo https://github.com/b2etw/Spring-Kotlin...

2021最新Canonical终极指南,短短的语法让你的SEO功力倍增提升网站能见度

Canonical 是什麽 图片来源:https://www.samunderwood.co.uk/...

python3-日历

在python3中,想要制作日历有两种方式,先介绍第一种: -直接使用python中calendar...

EP 9: Navigation by Shell in TopStore App

Hello, 各位 iT邦帮忙 的粉丝们大家好~~~ 本篇是 Re: 从零开始用 Xamarin 技...

[30天 Vue学好学满 DAY28] keep-alive 状态保留

简介 vue原生元件,可达到cache目的。 使元件状态维持不变,不重走生命周期。 新增钩子 act...