如果代替方向而是从三维空间中选一个点当作光源, 然后在着色器中根据光源和表面位置计算光照方向的话,就是点光源了。

    image.png

    如果旋转上方的表面会发现,每个点都有一个不同的面到光源的矢量, 将这个矢量和法向量点乘后,表面上的每个点都会有一个不同的光照值。
    让我们实现它吧。
    首先需要一个光源位置

    1. uniform vec3 u_lightWorldPosition;
    2. // 然后需要计算表面的世界坐标,我们可以将位置和世界矩阵相乘得到...
    3. uniform mat4 u_world;
    4. ...
    5. // 计算表面的世界坐标
    6. vec3 surfaceWorldPosition = (u_world * a_position).xyz;
    7. // 然后可以计算出一个从表面到光源的矢量,用来模拟之前的方向光, 只是这次我们为表面上的每个点都计算了一个方向。
    8. v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
    9. attribute vec4 a_position;
    10. attribute vec3 a_normal;
    11. uniform vec3 u_lightWorldPosition;
    12. uniform mat4 u_world;
    13. uniform mat4 u_worldViewProjection;
    14. uniform mat4 u_worldInverseTranspose;
    15. varying vec3 v_normal;
    16. varying vec3 v_surfaceToLight;
    17. void main() {
    18. // 将位置和矩阵相乘
    19. gl_Position = u_worldViewProjection * a_position;
    20. // 重定向法向量并传递给片断着色器
    21. v_normal = mat3(u_worldInverseTranspose) * a_normal;
    22. // 计算表面的世界坐标
    23. vec3 surfaceWorldPosition = (u_world * a_position).xyz;
    24. // 计算表面到光源的方向
    25. // 传递给片断着色器
    26. v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
    27. }

    在片断着色器中需要将表面到光源的方向进行单位化, 注意,虽然我们可以在顶点着色器中传递单位向量, 但是 varying 会进行插值再传给片断着色器, 所以片断着色器中的向量基本上不是单位向量了。

    1. precision mediump float;
    2. // 从顶点着色器中传入的值
    3. varying vec3 v_normal;
    4. varying vec3 v_surfaceToLight;
    5. uniform vec3 u_reverseLightDirection;
    6. uniform vec4 u_color;
    7. void main() {
    8. // 由于 v_normal 是可变量,所以经过插值后不再是单位向量,
    9. // 单位化后会成为单位向量
    10. vec3 normal = normalize(v_normal);
    11. vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
    12. -float light = dot(normal, u_reverseLightDirection);
    13. +float light = dot(normal, surfaceToLightDirection);
    14. gl_FragColor = u_color;
    15. // 只将颜色部分(不包含 alpha) 和光照相乘
    16. gl_FragColor.rgb *= light;
    17. }

    然后需要找到 u_world 和 u_lightWorldPosition 的位置
    image.png

    image.png

    现在我们可以加一个叫做镜面高光的东西。
    观察现实世界中的物体,如果物体表面恰好将光线反射到你眼前, 就会显得非常明亮,像镜子一样。
    我们可以通过计算光线是否反射到眼前来模拟这种情况,点乘又一次起到了至关重要的作用。
    如何测试呢?如果入射角和反射角恰好与眼睛和和光源的夹角相同,那么光线就会反射到眼前。

    image.png

    如果我们知道了物体表面到光源的方向(刚刚已经计算过了), 加上物体表面到视区/眼睛/相机的方向,再除以 2 得到 halfVector 向量, 将这个向量和法向量比较,如果方向一致,那么光线就会被反射到眼前。 那么如何确定方向是否一致呢?用之前的点乘就可以了。1 表示相符, 0 表示垂直,-1 表示相反。

    image.png
    所以首先我们需要传入相机位置,计算表面到相机的方向矢量, 然后传递到片断着色器。

    1. attribute vec4 a_position;
    2. attribute vec3 a_normal;
    3. uniform vec3 u_lightWorldPosition;
    4. uniform vec3 u_viewWorldPosition;
    5. uniform mat4 u_world;
    6. uniform mat4 u_worldViewProjection;
    7. uniform mat4 u_worldInverseTranspose;
    8. varying vec3 v_normal;
    9. varying vec3 v_surfaceToLight;
    10. varying vec3 v_surfaceToView;
    11. void main() {
    12. // 将位置和矩阵相乘
    13. gl_Position = u_worldViewProjection * a_position;
    14. // 重定向法向量并传递到片断着色器
    15. v_normal = mat3(u_worldInverseTranspose) * a_normal;
    16. // 计算表面的世界坐标
    17. vec3 surfaceWorldPosition = (u_world * a_position).xyz;
    18. // 计算表面到光源的方向
    19. // 然后传递到片断着色器
    20. v_surfaceToLight = u_lightWorldPosition - surfaceWorldPosition;
    21. // 计算表面到相机的方向
    22. // 然后传递到片断着色器
    23. v_surfaceToView = u_viewWorldPosition - surfaceWorldPosition;
    24. }

    然后在片断着色器中计算表面到光源和相机之间的 halfVector, 将它和法向量相乘,查看光线是否直接反射到眼前。

    1. // 从顶点着色器中传入的值
    2. varying vec3 v_normal;
    3. varying vec3 v_surfaceToLight;
    4. varying vec3 v_surfaceToView;
    5. uniform vec4 u_color;
    6. void main() {
    7. // 由于 v_normal 是可变量,所以经过插值后不再是单位向量,
    8. // 单位化后会成为单位向量
    9. vec3 normal = normalize(v_normal);
    10. vec3 surfaceToLightDirection = normalize(v_surfaceToLight);
    11. vec3 surfaceToViewDirection = normalize(v_surfaceToView);
    12. vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewDirection);
    13. float light = dot(normal, surfaceToLightDirection);
    14. float specular = dot(normal, halfVector);
    15. gl_FragColor = u_color;
    16. // 只将颜色部分(不包含 alpha) 和光照相乘
    17. gl_FragColor.rgb *= light;
    18. // 直接加上高光
    19. gl_FragColor.rgb += specular;
    20. }

    最后找到 u_viewWorldPosition 并设置它

    1. var lightWorldPositionLocation =
    2. gl.getUniformLocation(program, "u_lightWorldPosition");
    3. var viewWorldPositionLocation =
    4. gl.getUniformLocation(program, "u_viewWorldPosition");
    5. ...
    6. // 计算相机矩阵
    7. var camera = [100, 150, 200];
    8. var target = [0, 35, 0];
    9. var up = [0, 1, 0];
    10. var cameraMatrix = makeLookAt(camera, target, up);
    11. // 设置相机位置
    12. gl.uniform3fv(viewWorldPositionLocation, camera);

    但,亮瞎了!
    我们可以将点乘结果进行求幂运算来解决太亮的问题, 它会把高光从线性变换变成指数变换。
    image.png
    红线越接近顶部,我们加的光照就越多,通过求幂可以将高光的部分向右移动。
    就把它叫做 shininess 并加到着色器中。

    1. uniform vec4 u_color;
    2. uniform float u_shininess;
    3. ...
    4. float specular = dot(normal, halfVector);
    5. float specular = 0.0;
    6. if (light > 0.0) {
    7. specular = pow(dot(normal, halfVector), u_shininess);
    8. }

    点乘结果有可能为负值,将赋值求幂有可能会得到 undefined 的结果, 所以我们只将点乘结果为正的部分进行计算,其他部分设置为 0.0。
    当然还要找到亮度的位置并设置它

    1. var shininessLocation = gl.getUniformLocation(program, "u_shininess");
    2. ...
    3. // 设置亮度
    4. gl.uniform1f(shininessLocation, shininess);


    最后想说的是光的颜色。
    在此之前我们都是将 light 和 F 的颜色直接相乘, 如果想要有色光也可以为光照提供颜色。

    1. uniform vec4 u_color;
    2. uniform float u_shininess;
    3. uniform vec3 u_lightColor;
    4. uniform vec3 u_specularColor;
    5. ...
    6. // 只将颜色部分(不包含 alpha) 和光照相乘
    7. gl_FragColor.rgb *= light * u_lightColor;
    8. // 直接和高光相加
    9. var lightColorLocation =
    10. gl.getUniformLocation(program, "u_lightColor");
    11. var specularColorLocation =
    12. gl.getUniformLocation(program, "u_specularColor");
    13. // 设置光照颜色
    14. gl.uniform3fv(lightColorLocation, m4.normalize([1, 0.6, 0.6])); // 红光
    15. // 设置高光颜色
    16. gl.uniform3fv(specularColorLocation, m4.normalize([1, 0.6, 0.6])); // 红光