【视觉高级篇】25 # 如何用法线贴图模拟真实物体表面

news2024/11/25 4:25:35

说明

【跟月影学可视化】学习笔记。

什么是法线贴图?

法线贴图就是在原物体的凹凸表面的每个点上均作法线,通过RGB颜色通道来标记法线的方向,你可以把它理解成与原凹凸表面平行的另一个不同的表面,但实际上它又只是一个光滑的平面。对于视觉效果而言,它的效率比原有的凹凸表面更高,若在特定位置上应用光源,可以让细节程度较低的表面生成高细节程度的精确光照方向和反射效果。

什么是切线空间?

切线空间(Tangent Space)是一个特殊的坐标系,它是由几何体顶点所在平面的 uv 坐标和法线构成的。

切线空间的三个轴,一般用 T (Tangent)、B (Bitangent)、N (Normal) 三个字母表示,所以切线空间也被称为 TBN 空间。其中 T 表示切线、B 表示副切线、N 表示法线。

在这里插入图片描述

为什么需要切线空间?

法线是垂直于面的单位向量,当在贴图中记录法线时,其坐标系有如下选择:

  1. 世界坐标系:当面改变朝向改变时,贴图中的法线就失效了
  2. 模型坐标系:需要根据面的朝向,生成不同的法线贴图。想象一个立方体的砖头,他们的6个面的法线情况是一样的,也需要根据面的不同朝向生成对应的法线贴图

切线空间就是为了解决法线贴图的问题的。为了能统一使用一张法线贴图,就引出了切线空间。

切线空间中的 TBN 是怎么计算的?

数学推导过程比较复杂,有兴趣的可以看一下这篇法线贴图的文章:Normal Mapping

在这里插入图片描述
公式如下:

在这里插入图片描述

通过 UV 坐标和点 P1、P2、P3 的坐标求出对应的 T 和 B 坐标:

function createTB(geometry) {
  const {position, index, uv} = geometry.attributes;
  if(!uv) throw new Error('NO uv.');
  function getTBNTriangle(p1, p2, p3, uv1, uv2, uv3) {
    const edge1 = new Vec3().sub(p2, p1);
    const edge2 = new Vec3().sub(p3, p1);
    const deltaUV1 = new Vec2().sub(uv2, uv1);
    const deltaUV2 = new Vec2().sub(uv3, uv1);

    const tang = new Vec3();
    const bitang = new Vec3();

    const f = 1.0 / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

    tang.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
    tang.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
    tang.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);

    tang.normalize();

    bitang.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
    bitang.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
    bitang.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);

    bitang.normalize();

    return {tang, bitang};
  }

  const size = position.size;
  if(size < 3) throw new Error('Error dimension.');

  const len = position.data.length / size;
  const tang = new Float32Array(len * 3);
  const bitang = new Float32Array(len * 3);

  for(let i = 0; i < index.data.length; i += 3) {
    const i1 = index.data[i];
    const i2 = index.data[i + 1];
    const i3 = index.data[i + 2];

    const p1 = [position.data[i1 * size], position.data[i1 * size + 1], position.data[i1 * size + 2]];
    const p2 = [position.data[i2 * size], position.data[i2 * size + 1], position.data[i2 * size + 2]];
    const p3 = [position.data[i3 * size], position.data[i3 * size + 1], position.data[i3 * size + 2]];

    const u1 = [uv.data[i1 * 2], uv.data[i1 * 2 + 1]];
    const u2 = [uv.data[i2 * 2], uv.data[i2 * 2 + 1]];
    const u3 = [uv.data[i3 * 2], uv.data[i3 * 2 + 1]];

    const {tang: t, bitang: b} = getTBNTriangle(p1, p2, p3, u1, u2, u3);
    tang.set(t, i1 * 3);
    tang.set(t, i2 * 3);
    tang.set(t, i3 * 3);
    bitang.set(b, i1 * 3);
    bitang.set(b, i2 * 3);
    bitang.set(b, i3 * 3);
  }
  geometry.addAttribute('tang', {data: tang, size: 3});
  geometry.addAttribute('bitang', {data: bitang, size: 3});
  return geometry;
}

构建 TBN 矩阵来计算法向量

TBN 矩阵的作用,就是将法线贴图里面读取的法向量数据,转换为对应的切线空间中实际的法向量。

顶点着色器增加了 tang 和 bitang 这两个属性,这里使用 WebGL2.0 的写法,它对应是 OpenGL ES3.0

里面用 in 和 out 对应变量的输入和输出,来取代 OpenGL ES2.0 的 attribute 和 varying。

顶点着色器:

#version 300 es
precision highp float;

in vec3 position;
in vec3 normal;
in vec2 uv;
in vec3 tang;
in vec3 bitang;

uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform mat3 normalMatrix;
uniform vec3 cameraPosition;

out vec3 vNormal;
out vec3 vPos;
out vec2 vUv;
out vec3 vCameraPos;
out mat3 vTBN;

void main() {
  vec4 pos = modelViewMatrix * vec4(position, 1.0);
  vPos = pos.xyz;
  vUv = uv;
  vCameraPos = (viewMatrix * vec4(cameraPosition, 1.0)).xyz;
  vNormal = normalize(normalMatrix * normal);

  vec3 N = vNormal;
  vec3 T = normalize(normalMatrix * tang);
  vec3 B = normalize(normalMatrix * bitang);

  vTBN = mat3(T, B, N);
  
  gl_Position = projectionMatrix * pos;
}

片元着色器:


#version 300 es
precision highp float;

#define MAX_LIGHT_COUNT 16
uniform mat4 viewMatrix;

uniform vec3 ambientLight;
uniform vec3 directionalLightDirection[MAX_LIGHT_COUNT];
uniform vec3 directionalLightColor[MAX_LIGHT_COUNT];
uniform vec3 pointLightColor[MAX_LIGHT_COUNT];
uniform vec3 pointLightPosition[MAX_LIGHT_COUNT];
uniform vec3 pointLightDecay[MAX_LIGHT_COUNT];
uniform vec3 spotLightColor[MAX_LIGHT_COUNT];
uniform vec3 spotLightDirection[MAX_LIGHT_COUNT];
uniform vec3 spotLightPosition[MAX_LIGHT_COUNT];
uniform vec3 spotLightDecay[MAX_LIGHT_COUNT];
uniform float spotLightAngle[MAX_LIGHT_COUNT];

uniform vec3 materialReflection;
uniform float shininess;
uniform float specularFactor;

uniform sampler2D tNormal;

in vec3 vNormal;
in vec3 vPos;
in vec2 vUv;
in vec3 vCameraPos;
in mat3 vTBN;

out vec4 FragColor;

float getSpecular(vec3 dir, vec3 normal, vec3 eye) {
  vec3 reflectionLight = reflect(-dir, normal);
  float eyeCos = max(dot(eye, reflectionLight), 0.0);
  return specularFactor *  pow(eyeCos, shininess);
}

vec4 phongReflection(vec3 pos, vec3 normal, vec3 eye) {
  float specular = 0.0;
  vec3 diffuse = vec3(0);
  
  // 处理平行光
  for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
    vec3 dir = directionalLightDirection[i];
    if(dir.x == 0.0 && dir.y == 0.0 && dir.z == 0.0) continue;
    vec4 d = viewMatrix * vec4(dir, 0.0);
    dir = normalize(-d.xyz);
    float cos = max(dot(dir, normal), 0.0);
    diffuse += cos * directionalLightColor[i];
    specular += getSpecular(dir, normal, eye);
  }

  // 处理点光源
  for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
    vec3 decay = pointLightDecay[i];
    if(decay.x == 0.0 && decay.y == 0.0 && decay.z == 0.0) continue;
    vec3 dir = (viewMatrix * vec4(pointLightPosition[i], 1.0)).xyz - pos;
    float dis = length(dir);
    dir = normalize(dir);
    float cos = max(dot(dir, normal), 0.0);
    float d = min(1.0, 1.0 / (decay.x * pow(dis, 2.0) + decay.y * dis + decay.z));
    diffuse += d * cos * pointLightColor[i];
    specular += getSpecular(dir, normal, eye);
  }

  // 处理聚光灯
  for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
    vec3 decay = spotLightDecay[i];
    if(decay.x == 0.0 && decay.y == 0.0 && decay.z == 0.0) continue;

    vec3 dir = (viewMatrix * vec4(spotLightPosition[i], 1.0)).xyz - pos;
    float dis = length(dir);
    dir = normalize(dir);

    // 聚光灯的朝向
    vec3 spotDir = (viewMatrix * vec4(spotLightDirection[i], 0.0)).xyz;
    // 通过余弦值判断夹角范围
    float ang = cos(spotLightAngle[i]);
    float r = step(ang, dot(dir, normalize(-spotDir)));

    float cos = max(dot(dir, normal), 0.0);
    float d = min(1.0, 1.0 / (decay.x * pow(dis, 2.0) + decay.y * dis + decay.z));
    diffuse += r * d * cos * spotLightColor[i];
    specular += r * getSpecular(dir, normal, eye);
  }

  return vec4(diffuse, specular);
}

vec3 getNormal() {
  vec3 n = texture(tNormal, vUv).rgb * 2.0 - 1.0;
  return normalize(vTBN * n);
}

void main() {
  vec3 eyeDirection = normalize(vCameraPos - vPos);
  vec3 normal = getNormal();
  vec4 phong = phongReflection(vPos, normal, eyeDirection);

  // 合成颜色
  FragColor.rgb = phong.w + (phong.xyz + ambientLight) * materialReflection;
  FragColor.a = 1.0;
}

通过顶点数据计算几何体的切线和副切线,然后得到 TBN 矩阵,用 TBN 矩阵和法线纹理数据来计算法向量,从而完成法线贴图。

注意:着色器里需要加 #version 300 es ,不然会报错,它表示这段代码是 OpenGL ES3.0 的。

在这里插入图片描述
可以参考这篇:glsl version 300es 关键字

使用偏导数来实现法线贴图

用顶点数据计算几何体的切线和副切线相对来说是复杂了一点,还可以使用坐标插值和法线纹理来计算。

具体如下:其中 dFdxdFdy 是 GLSL 内置函数,可以求插值的属性在 x、y 轴上的偏导数。

vec3 getNormal() {
	vec3 pos_dx = dFdx(vPos.xyz);
	vec3 pos_dy = dFdy(vPos.xyz);
	vec2 tex_dx = dFdx(vUv);
	vec2 tex_dy = dFdy(vUv);
	
	vec3 t = normalize(pos_dx * tex_dy.t - pos_dy * tex_dx.t);
	vec3 b = normalize(-pos_dx * tex_dy.s + pos_dy * tex_dx.s);
	mat3 tbn = mat3(t, b, normalize(vNormal));
	
	vec3 n = texture(tNormal, vUv).rgb * 2.0 - 1.0;
	return normalize(tbn * n);
}

偏导数代表插值的属性向量在 x、y 轴上的变化率,或者说曲面的切线。再将顶点坐标曲面切线与 uv 坐标的切线求叉积,就能得到垂直于两条切线的法线。对应 TBN 空间的切线 tang 和副切线 bitang。然后使用偏导数构建 TBN 矩阵,把 TBN 矩阵左乘从法线纹理中提取出的值,就可以计算出对应的法向量。不过计算偏导数也有一定的性能开销。

如何使用法线贴图给几何体表面增加凹凸效果?

下面我们准备这个图当纹理,实现一个正方体的法线贴图:

在这里插入图片描述

代码如下:这里使用的是偏导数来实现的

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>如何使用法线贴图给几何体表面增加凹凸效果</title>
        <style>
            canvas {
                border: 1px dashed #fa8072;
            }
        </style>
    </head>
    <body>
        <canvas width="512" height="512"></canvas>
        <script type="module">
            import { Renderer, Camera, Transform, Box, Orbit, Program, Texture, Mesh, Color, Vec3, Vec2 } from './common/lib/ogl/index.mjs';
            import { Phong, Material } from './common/lib/phong.js';

            const vertex = `#version 300 es
                precision highp float;

                in vec3 position;
                in vec3 normal;
                in vec2 uv;
                in vec3 tang;
                in vec3 bitang;

                uniform mat4 modelMatrix;
                uniform mat4 modelViewMatrix;
                uniform mat4 viewMatrix;
                uniform mat4 projectionMatrix;
                uniform mat3 normalMatrix;
                uniform vec3 cameraPosition;

                out vec3 vNormal;
                out vec3 vPos;
                out vec2 vUv;
                out vec3 vCameraPos;
                out mat3 vTBN;

                void main() {
                    vec4 pos = modelViewMatrix * vec4(position, 1.0);
                    vPos = pos.xyz;
                    vUv = uv;
                    vCameraPos = (viewMatrix * vec4(cameraPosition, 1.0)).xyz;
                    vNormal = normalize(normalMatrix * normal);

                    vec3 N = vNormal;
                    vec3 T = normalize(normalMatrix * tang);
                    vec3 B = normalize(normalMatrix * bitang);

                    vTBN = mat3(T, B, N);
                    
                    gl_Position = projectionMatrix * pos;
                }
            `;

            const fragment = `#version 300 es
                precision highp float;

                #define MAX_LIGHT_COUNT 16
                uniform mat4 viewMatrix;

                uniform vec3 ambientLight;
                uniform vec3 directionalLightDirection[MAX_LIGHT_COUNT];
                uniform vec3 directionalLightColor[MAX_LIGHT_COUNT];
                uniform vec3 pointLightColor[MAX_LIGHT_COUNT];
                uniform vec3 pointLightPosition[MAX_LIGHT_COUNT];
                uniform vec3 pointLightDecay[MAX_LIGHT_COUNT];
                uniform vec3 spotLightColor[MAX_LIGHT_COUNT];
                uniform vec3 spotLightDirection[MAX_LIGHT_COUNT];
                uniform vec3 spotLightPosition[MAX_LIGHT_COUNT];
                uniform vec3 spotLightDecay[MAX_LIGHT_COUNT];
                uniform float spotLightAngle[MAX_LIGHT_COUNT];

                uniform vec3 materialReflection;
                uniform float shininess;
                uniform float specularFactor;

                uniform sampler2D tNormal;

                in vec3 vNormal;
                in vec3 vPos;
                in vec2 vUv;
                in vec3 vCameraPos;
                in mat3 vTBN;

                out vec4 FragColor;

                float getSpecular(vec3 dir, vec3 normal, vec3 eye) {
                    vec3 reflectionLight = reflect(-dir, normal);
                    float eyeCos = max(dot(eye, reflectionLight), 0.0);
                    return specularFactor *  pow(eyeCos, shininess);
                }

                vec4 phongReflection(vec3 pos, vec3 normal, vec3 eye) {
                    float specular = 0.0;
                    vec3 diffuse = vec3(0);
                    
                    // 处理平行光
                    for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
                        vec3 dir = directionalLightDirection[i];
                        if(dir.x == 0.0 && dir.y == 0.0 && dir.z == 0.0) continue;
                        vec4 d = viewMatrix * vec4(dir, 0.0);
                        dir = normalize(-d.xyz);
                        float cos = max(dot(dir, normal), 0.0);
                        diffuse += cos * directionalLightColor[i];
                        specular += getSpecular(dir, normal, eye);
                    }

                    // 处理点光源
                    for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
                        vec3 decay = pointLightDecay[i];
                        if(decay.x == 0.0 && decay.y == 0.0 && decay.z == 0.0) continue;
                        vec3 dir = (viewMatrix * vec4(pointLightPosition[i], 1.0)).xyz - pos;
                        float dis = length(dir);
                        dir = normalize(dir);
                        float cos = max(dot(dir, normal), 0.0);
                        float d = min(1.0, 1.0 / (decay.x * pow(dis, 2.0) + decay.y * dis + decay.z));
                        diffuse += d * cos * pointLightColor[i];
                        specular += getSpecular(dir, normal, eye);
                    }

                    // 处理聚光灯
                    for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
                        vec3 decay = spotLightDecay[i];
                        if(decay.x == 0.0 && decay.y == 0.0 && decay.z == 0.0) continue;

                        vec3 dir = (viewMatrix * vec4(spotLightPosition[i], 1.0)).xyz - pos;
                        float dis = length(dir);
                        dir = normalize(dir);

                        // 聚光灯的朝向
                        vec3 spotDir = (viewMatrix * vec4(spotLightDirection[i], 0.0)).xyz;
                        // 通过余弦值判断夹角范围
                        float ang = cos(spotLightAngle[i]);
                        float r = step(ang, dot(dir, normalize(-spotDir)));

                        float cos = max(dot(dir, normal), 0.0);
                        float d = min(1.0, 1.0 / (decay.x * pow(dis, 2.0) + decay.y * dis + decay.z));
                        diffuse += r * d * cos * spotLightColor[i];
                        specular += r * getSpecular(dir, normal, eye);
                    }

                    return vec4(diffuse, specular);
                }

                // vec3 getNormal() {
                //     vec3 n = texture(tNormal, vUv).rgb * 2.0 - 1.0;
                //     return normalize(vTBN * n);
                // }

                vec3 getNormal() {
                    vec3 pos_dx = dFdx(vPos.xyz);
                    vec3 pos_dy = dFdy(vPos.xyz);
                    vec2 tex_dx = dFdx(vUv);
                    vec2 tex_dy = dFdy(vUv);

                    vec3 t = normalize(pos_dx * tex_dy.t - pos_dy * tex_dx.t);
                    vec3 b = normalize(-pos_dx * tex_dy.s + pos_dy * tex_dx.s);
                    mat3 tbn = mat3(t, b, normalize(vNormal));

                    vec3 n = texture(tNormal, vUv).rgb * 2.0 - 1.0;
                    return normalize(tbn * n);
                }

                void main() {
                    vec3 eyeDirection = normalize(vCameraPos - vPos);
                    vec3 normal = getNormal();
                    vec4 phong = phongReflection(vPos, normal, eyeDirection);

                    // 合成颜色
                    FragColor.rgb = phong.w + (phong.xyz + ambientLight) * materialReflection;
                    FragColor.a = 1.0;
                }
            `;

            // 通过 UV 坐标和点 P1、P2、P3 的坐标求出对应的 T 和 B 坐标,将 tang 和 bitang 的值添加到 geometry 对象中去
            function createTB(geometry) {
                const {position, index, uv} = geometry.attributes;
                if(!uv) throw new Error('NO uv.');
                function getTBNTriangle(p1, p2, p3, uv1, uv2, uv3) {
                    const edge1 = new Vec3().sub(p2, p1);
                    const edge2 = new Vec3().sub(p3, p1);
                    const deltaUV1 = new Vec2().sub(uv2, uv1);
                    const deltaUV2 = new Vec2().sub(uv3, uv1);

                    const tang = new Vec3();
                    const bitang = new Vec3();

                    const f = 1.0 / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

                    tang.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
                    tang.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
                    tang.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);

                    tang.normalize();

                    bitang.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
                    bitang.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
                    bitang.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);

                    bitang.normalize();

                    return {tang, bitang};
                }

                const size = position.size;
                if(size < 3) throw new Error('Error dimension.');

                const len = position.data.length / size;
                const tang = new Float32Array(len * 3);
                const bitang = new Float32Array(len * 3);

                for(let i = 0; i < index.data.length; i += 3) {
                    const i1 = index.data[i];
                    const i2 = index.data[i + 1];
                    const i3 = index.data[i + 2];

                    const p1 = [position.data[i1 * size], position.data[i1 * size + 1], position.data[i1 * size + 2]];
                    const p2 = [position.data[i2 * size], position.data[i2 * size + 1], position.data[i2 * size + 2]];
                    const p3 = [position.data[i3 * size], position.data[i3 * size + 1], position.data[i3 * size + 2]];

                    const u1 = [uv.data[i1 * 2], uv.data[i1 * 2 + 1]];
                    const u2 = [uv.data[i2 * 2], uv.data[i2 * 2 + 1]];
                    const u3 = [uv.data[i3 * 2], uv.data[i3 * 2 + 1]];

                    const {tang: t, bitang: b} = getTBNTriangle(p1, p2, p3, u1, u2, u3);
                    tang.set(t, i1 * 3);
                    tang.set(t, i2 * 3);
                    tang.set(t, i3 * 3);
                    bitang.set(b, i1 * 3);
                    bitang.set(b, i2 * 3);
                    bitang.set(b, i3 * 3);
                }
                geometry.addAttribute('tang', {data: tang, size: 3});
                geometry.addAttribute('bitang', {data: bitang, size: 3});
                return geometry;
            }

            // 加载纹理
            function loadTexture(src) {
                const texture = new Texture(gl);
                return new Promise((resolve) => {
                    const img = new Image();
                    img.onload = () => {
                        texture.image = img;
                        resolve(texture);
                    };
                    img.src = src;
                });
            }

            const canvas = document.querySelector('canvas');
            const renderer = new Renderer({
                canvas,
                width: 512,
                height: 512,
            });

            const gl = renderer.gl;
            gl.clearColor(1, 1, 1, 1);
            const camera = new Camera(gl, {fov: 6});
            camera.position.set(10, 10, 10);
            camera.lookAt([0, 0, 0]);

            const scene = new Transform();
            
            // 用 Phong 反射模型绘制一个立方体,并给它添加两道平行光
            const phong = new Phong();
            // 添加平行光
            phong.addLight({
                direction: [0, -3, -3]
            });
            phong.addLight({
                direction: [3, 3, 0]
            });

            // 创建 matrial 对象
            const matrial = new Material(new Color('#2e8b57'), 2.0);

            (async function () {
                const normalMap = await loadTexture('./assets/25/normal_mapping_normal_map.png');
                console.log(normalMap);
                const program = new Program(gl, {
                    vertex,
                    fragment,
                    uniforms: {
                        ...matrial.uniforms,
                        ...phong.uniforms,
                        tNormal: {
                            value: normalMap
                        }
                    },
                });

                // 创建几何体
                const geometry = new Box(gl);
                createTB(geometry);

                const cube = new Mesh(gl, {geometry, program});
                cube.setParent(scene);
                cube.rotation.x = -Math.PI / 2;

                const controls = new Orbit(camera);
                // 添加动画
                requestAnimationFrame(update);
                function update() {
                    requestAnimationFrame(update);
                    controls.update();
                    renderer.render({scene, camera});
                }
            })();
        </script>
    </body>
</html>

在这里插入图片描述

法线贴图的应用

法线贴图除了给几何体表面增加花纹以外,还可以用来增强物体细节,让物体看起来更加真实。

下面实现一个石块被变化的光源照亮效果的时候,运用法线贴图技术,让石块的表面纹路细节显得更逼真。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>法线贴图的应用</title>
        <style>
            canvas {
                border: 1px dashed #fa8072;
            }
        </style>
    </head>
    <body>
        <canvas width="512" height="512"></canvas>
        <script type="module">
            import { Renderer, Camera, Transform, Geometry, Orbit, Program, Texture, Mesh, Color, Vec3, Vec2 } from './common/lib/ogl/index.mjs';
            import { Phong, Material } from './common/lib/phong.js';

            const vertex = `#version 300 es
                precision highp float;

                in vec3 position;
                in vec3 normal;
                in vec2 uv;
                in vec3 tang;
                in vec3 bitang;

                uniform mat4 modelMatrix;
                uniform mat4 modelViewMatrix;
                uniform mat4 viewMatrix;
                uniform mat4 projectionMatrix;
                uniform mat3 normalMatrix;
                uniform vec3 cameraPosition;

                out vec3 vNormal;
                out vec3 vPos;
                out vec2 vUv;
                out vec3 vCameraPos;
                out mat3 vTBN;

                void main() {
                    vec4 pos = modelViewMatrix * vec4(position, 1.0);
                    vPos = pos.xyz;
                    vUv = uv;
                    vCameraPos = (viewMatrix * vec4(cameraPosition, 1.0)).xyz;
                    vNormal = normalize(normalMatrix * normal);

                    vec3 N = vNormal;
                    vec3 T = normalize(normalMatrix * tang);
                    vec3 B = normalize(normalMatrix * bitang);

                    vTBN = mat3(T, B, N);
                    
                    gl_Position = projectionMatrix * pos;
                }
            `;

            const fragment = `#version 300 es
                precision highp float;

                #define MAX_LIGHT_COUNT 16
                uniform mat4 viewMatrix;

                uniform vec3 ambientLight;
                uniform vec3 directionalLightDirection[MAX_LIGHT_COUNT];
                uniform vec3 directionalLightColor[MAX_LIGHT_COUNT];
                uniform vec3 pointLightColor[MAX_LIGHT_COUNT];
                uniform vec3 pointLightPosition[MAX_LIGHT_COUNT];
                uniform vec3 pointLightDecay[MAX_LIGHT_COUNT];
                uniform vec3 spotLightColor[MAX_LIGHT_COUNT];
                uniform vec3 spotLightDirection[MAX_LIGHT_COUNT];
                uniform vec3 spotLightPosition[MAX_LIGHT_COUNT];
                uniform vec3 spotLightDecay[MAX_LIGHT_COUNT];
                uniform float spotLightAngle[MAX_LIGHT_COUNT];

                uniform vec3 materialReflection;
                uniform float shininess;
                uniform float specularFactor;

                uniform sampler2D tMap;
                uniform sampler2D tNormal;

                uniform float uTime;

                in vec3 vNormal;
                in vec3 vPos;
                in vec2 vUv;
                in vec3 vCameraPos;
                in mat3 vTBN;

                out vec4 FragColor;

                float getSpecular(vec3 dir, vec3 normal, vec3 eye) {
                    vec3 reflectionLight = reflect(-dir, normal);
                    float eyeCos = max(dot(eye, reflectionLight), 0.0);
                    return specularFactor *  pow(eyeCos, shininess);
                }

                vec4 phongReflection(vec3 pos, vec3 normal, vec3 eye) {
                    float specular = 0.0;
                    vec3 diffuse = vec3(0);
                    
                    // 处理平行光
                    for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
                        vec3 dir = directionalLightDirection[i];
                        if(dir.x == 0.0 && dir.y == 0.0 && dir.z == 0.0) continue;
                        vec4 d = viewMatrix * vec4(dir, 0.0);
                        dir = normalize(-d.xyz);
                        float cos = max(dot(dir, normal), 0.0);
                        diffuse += cos * directionalLightColor[i];
                        specular += getSpecular(dir, normal, eye);
                    }

                    // 处理点光源
                    for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
                        vec3 decay = pointLightDecay[i];
                        if(decay.x == 0.0 && decay.y == 0.0 && decay.z == 0.0) continue;
                        vec3 dir = (viewMatrix * vec4(pointLightPosition[i], 1.0)).xyz - pos;
                        float dis = length(dir);
                        dir = normalize(dir);
                        float cos = max(dot(dir, normal), 0.0);
                        float d = min(1.0, 1.0 / (decay.x * pow(dis, 2.0) + decay.y * dis + decay.z));
                        diffuse += d * cos * pointLightColor[i];
                        specular += getSpecular(dir, normal, eye);
                    }

                    // 处理聚光灯
                    for(int i = 0; i < MAX_LIGHT_COUNT; i++) {
                        vec3 decay = spotLightDecay[i];
                        if(decay.x == 0.0 && decay.y == 0.0 && decay.z == 0.0) continue;

                        vec3 dir = (viewMatrix * vec4(spotLightPosition[i], 1.0)).xyz - pos;
                        float dis = length(dir);
                        dir = normalize(dir);

                        // 聚光灯的朝向
                        vec3 spotDir = (viewMatrix * vec4(spotLightDirection[i], 0.0)).xyz;
                        // 通过余弦值判断夹角范围
                        float ang = cos(spotLightAngle[i]);
                        float r = step(ang, dot(dir, normalize(-spotDir)));

                        float cos = max(dot(dir, normal), 0.0);
                        float d = min(1.0, 1.0 / (decay.x * pow(dis, 2.0) + decay.y * dis + decay.z));
                        diffuse += r * d * cos * spotLightColor[i];
                        specular += r * getSpecular(dir, normal, eye);
                    }

                    return vec4(diffuse, specular);
                }

                vec3 getNormal() {
                    vec3 n = texture(tNormal, vUv).rgb * 2.0 - 1.0;
                    return normalize(vTBN * n);
                }

                // vec3 getNormal() {
                //     vec3 pos_dx = dFdx(vPos.xyz);
                //     vec3 pos_dy = dFdy(vPos.xyz);
                //     vec2 tex_dx = dFdx(vUv);
                //     vec2 tex_dy = dFdy(vUv);

                //     vec3 t = normalize(pos_dx * tex_dy.t - pos_dy * tex_dx.t);
                //     vec3 b = normalize(-pos_dx * tex_dy.s + pos_dy * tex_dx.s);
                //     mat3 tbn = mat3(t, b, normalize(vNormal));

                //     vec3 n = texture(tNormal, vUv).rgb * 2.0 - 1.0;
                //     return normalize(tbn * n);
                // }

                void main() {
                    vec3 eyeDirection = normalize(vCameraPos - vPos);
                    vec3 normal = getNormal();
                    vec4 phong = phongReflection(vPos, normal, eyeDirection);
                    // vec4 phong = phongReflection(vPos, vNormal, eyeDirection);

                    vec3 tex = texture(tMap, vUv).rgb;
                    vec3 light = normalize(vec3(sin(uTime), 1.0, cos(uTime)));
                    float shading = dot(normal, light) * 0.5;

                    // 合成颜色
                    FragColor.rgb = phong.w + (phong.xyz + ambientLight) * materialReflection;
                    FragColor.a = 1.0;
                }
            `;

            // 通过 UV 坐标和点 P1、P2、P3 的坐标求出对应的 T 和 B 坐标,将 tang 和 bitang 的值添加到 geometry 对象中去
            function createTB(geometry) {
                const {position, index, uv} = geometry.attributes;
                if(!uv) throw new Error('NO uv.');
                function getTBNTriangle(p1, p2, p3, uv1, uv2, uv3) {
                    const edge1 = new Vec3().sub(p2, p1);
                    const edge2 = new Vec3().sub(p3, p1);
                    const deltaUV1 = new Vec2().sub(uv2, uv1);
                    const deltaUV2 = new Vec2().sub(uv3, uv1);

                    const tang = new Vec3();
                    const bitang = new Vec3();

                    const f = 1.0 / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

                    tang.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
                    tang.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
                    tang.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);

                    tang.normalize();

                    bitang.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
                    bitang.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
                    bitang.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);

                    bitang.normalize();

                    return {tang, bitang};
                }

                const size = position.size;
                if(size < 3) throw new Error('Error dimension.');

                const len = position.data.length / size;
                const tang = new Float32Array(len * 3);
                const bitang = new Float32Array(len * 3);

                const ilen = index ? index.data.length : len;

                for(let i = 0; i < ilen; i += 3) {
                    const i1 = index ? index.data[i] : i;
                    const i2 = index ? index.data[i + 1] : i + 1;
                    const i3 = index ? index.data[i + 2] : i + 2;

                    const p1 = [position.data[i1 * size], position.data[i1 * size + 1], position.data[i1 * size + 2]];
                    const p2 = [position.data[i2 * size], position.data[i2 * size + 1], position.data[i2 * size + 2]];
                    const p3 = [position.data[i3 * size], position.data[i3 * size + 1], position.data[i3 * size + 2]];

                    const u1 = [uv.data[i1 * 2], uv.data[i1 * 2 + 1]];
                    const u2 = [uv.data[i2 * 2], uv.data[i2 * 2 + 1]];
                    const u3 = [uv.data[i3 * 2], uv.data[i3 * 2 + 1]];

                    const {tang: t, bitang: b} = getTBNTriangle(p1, p2, p3, u1, u2, u3);
                    tang.set(t, i1 * 3);
                    tang.set(t, i2 * 3);
                    tang.set(t, i3 * 3);
                    bitang.set(b, i1 * 3);
                    bitang.set(b, i2 * 3);
                    bitang.set(b, i3 * 3);
                }
                geometry.addAttribute('tang', {data: tang, size: 3});
                geometry.addAttribute('bitang', {data: bitang, size: 3});
                return geometry;
            }

            // 加载纹理
            function loadTexture(src) {
                const texture = new Texture(gl);
                return new Promise((resolve) => {
                    const img = new Image();
                    img.onload = () => {
                        texture.image = img;
                        resolve(texture);
                    };
                    img.src = src;
                });
            }

            const canvas = document.querySelector('canvas');
            const renderer = new Renderer({
                canvas,
                width: 512,
                height: 512,
            });

            const gl = renderer.gl;
            gl.clearColor(1, 1, 1, 1);
            const camera = new Camera(gl, {fov: 6});
            camera.position.set(10, 10, 10);
            camera.lookAt([0, 0, 0]);

            const scene = new Transform();
            
            // 用 Phong 反射模型绘制一个立方体,并给它添加两道平行光
            const phong = new Phong();
            // 添加平行光
            phong.addLight({
                direction: [0, -3, -3]
            });
            phong.addLight({
                direction: [3, 3, 0]
            });

            // 创建 matrial 对象
            const matrial = new Material(new Color('#2e8b57'), 2.0);

            (async function () {
                const data = await (await fetch('./assets/25/rounded-cube.json')).json();
                const normalMap = await loadTexture('./assets/25/rock_normal.jpg');
                const texture = await loadTexture('./assets/25/rock.jpg');
                console.log(normalMap);
                const program = new Program(gl, {
                    vertex,
                    fragment,
                    uniforms: {
                        ...matrial.uniforms,
                        ...phong.uniforms,
                        tNormal: {
                            value: normalMap
                        },
                        uTime: {value: 0},
                        tMap: {value: texture},
                    },
                });

                // 创建几何体
                const geometry = new Geometry(gl, {
                    position: {size: 3, data: new Float32Array(data.position)},
                    uv: {size: 2, data: new Float32Array(data.uv)},
                    normal: {size: 3, data: new Float32Array(data.normal)},
                });
                createTB(geometry);

                const cube = new Mesh(gl, {geometry, program});
                cube.setParent(scene);
                cube.rotation.x = -Math.PI / 2;

                const controls = new Orbit(camera);
                // 添加动画
                requestAnimationFrame(update);
                function update(t) {
                    requestAnimationFrame(update);
                    controls.update();
                    program.uniforms.uTime.value = t * 0.001;
                    renderer.render({scene, camera});
                }
            })();
        </script>
    </body>
</html>

效果如下:大家看看是不是很像包浆的绿色石头。【手动狗头】

在这里插入图片描述

参考

  • 法线贴图
  • Normal-Mapping
  • 切线空间的理解(Tangent space)
  • glsl version 300es 关键字

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/89329.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

巧用 Chrome:网络知多少

开发者如数家珍的工具中&#xff0c;Chrome 想必是众多人心目中的白月光&#xff0c;倒也不是它有多么优秀&#xff0c;而是多亏同行浏览器们的衬托。其开源的内核 Chromium 也成就众多养家糊口的岗位&#xff0c;比如 Edge、Opera、QQ 浏览器、360 浏览器等等国内外一票浏览器…

物联网开发笔记(62)- 使用Micropython开发ESP32开发板之控制ILI9341 3.2寸TFT-LCD触摸屏进行LVGL图形化编程:环境搭建

一、目的 这一节我们学习如何使用我们的ESP32开发板来控制ILI9341 3.2寸TFT-LCD触摸屏进行LVGL图形化编程的第一步&#xff1a;环境搭建。 关键字&#xff1a;3.2寸SPI串口TFT液晶显示屏模块 ILI9341驱动 LCD触摸屏 240*320 LVGL图形化编程 XPT2046触摸屏芯片IC 二、环境 ESP…

实机安装CentOS7.9操作系统图文(保姆级)教程

一、制作启动U盘 1、下载Ventoy软件 去Ventoy官网下载Ventoy软件&#xff08;Download . Ventoy&#xff09;如下图界面 ​ 2、制作启动盘 选择合适的版本以及平台下载好之后&#xff0c;进行解压&#xff0c;解压出来之后进入文件夹&#xff0c;如下图左边所示&#xff0c…

Hive 之数据透视表

文章目录什么是数据透视表&#xff1f;创建数据源基于各产品在各个平台半年内的月销售额与汇总&#xff0c;制作数据透视表什么是数据透视表&#xff1f; 数据透视表是一种工具&#xff0c;用于帮助用户理解和分析大量数据。它通常是一个二维表格&#xff0c;可以让用户以不同…

java计算机毕业设计springboot+vue航空公司电子售票系统-机票预订系统

项目介绍 通篇文章的撰写基础是实际的应用需要,然后在架构系统之前全面复习大学所修习的相关知识以及网络提供的技术应用教程,以远程教育系统的实际应用需要出发,架构系统来改善现远程教育系统工作流程繁琐等问题。不仅如此以操作者的角度来说,该系统的架构能够对多媒体课程进…

手把手教你使用SpringBoot做一个员工管理系统【代码篇·下】

手把手教你使用SpringBoot做一个员工管理系统【代码篇下】1.增加员工实现2.修改员工信息3.删除员工4.404页面配置5.注销1.增加员工实现 新增添加员工的按钮&#xff1a; <h2><a class"btn btn-sm btn-success" th:href"{/addemp}">添加员工&…

0- LVGL移植基于野火STM32F429挑战者(LVGL8.2)

1-移植准备 LVGL8.2 野火STM32F429_v2开发板 因为ST在STM32F4之后所有的芯片都不在有标准库,因此本篇是基于HAL库的。同时现在有许多厂商都不在有标准库了,都是根据自己的开发环境进行一些基本芯片接口的配置。像NXP,ST等。 这里不过多介绍LVGL,既然看到这个文章,大多数是…

20221214英语学习

今日新词&#xff1a; minus prep.减去&#xff1b;&#xff08;温度&#xff09;零下 garlic n.【植】大蒜 linger v.停留&#xff0c;逗留&#xff1b;徘徊&#xff1b;继续留存&#xff0c;缓慢消失&#xff1b;苟延残喘 sarcastic adj.讽刺的, 嘲讽的, 挖苦的 data n.…

【LeetCode每日一题】——572.另一棵树的子树

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【时间频度】九【代码实现】十【提交结果】一【题目类别】 树 二【题目难度】 简单 三【题目编号】 572.另一棵树的子树 四【题目描述】 给你两棵二叉树…

MySQL索引优化(二)

文章目录一、查询优化1. 索引失效&#xff08;1&#xff09;不满足最左前缀法则&#xff0c;索引失效&#xff08;2&#xff09;在索引列上做任何计算、函数操作&#xff0c;索引失效&#xff08;3&#xff09;存储引擎使用索引中范围条件右边的列&#xff0c;索引失效&#xf…

倪健中会长应邀出席首届世界数贸易博览会致辞:把杭州打造成全球数字贸易元宇宙之都

12月11日至14日&#xff0c;首届全球数字贸易博览会在浙江省杭州市盛大举办。博览会由浙江省人民政府和商务部联合主办&#xff0c;杭州市人民政府、浙江省商务厅和商务部贸发局共同承办&#xff0c;主题为“数字贸易商通全球”&#xff0c;爱尔兰为主宾国&#xff0c;北京、上…

大航海时代:葡萄牙、西班牙率先出发,英国为何成为最大赢家?

欧洲经历了长达千年的中世纪以后&#xff0c;忽然开始自我反省了。为啥&#xff1f;因为打了上千年&#xff0c;不仅社会没进步&#xff0c;反而因为各种瘟疫、战争&#xff0c;把人口搞掉了一大半。 这么玩下去&#xff0c;日耳曼人的各大分支&#xff0c;可能都要完犊子了&a…

基于java+springboot+mybatis+vue+mysql的智能热度分析和自媒体推送平台

项目介绍 前端页面&#xff1a; 功能&#xff1a;首页、文章信息、图片信息、视频信息、个人中心、后台管理 管理员后台管理页面&#xff1a; 功能&#xff1a;首页、个人中心、用户管理、文章类型管理、文章信息管理、图片类型管理、图片信息管理、视频类型管理、视频信息管…

数据工厂刷新PowerBI数据集2

前面已经介绍过数据工厂中刷新PowerBI数据集&#xff0c;我们先发起一个web请示获取了token&#xff0c;然后再把token传入到接口中从而刷新数据集。 但是&#xff0c;明明都是微软家的产品&#xff0c;竟然还需要先获取token?明明Power Apps、Power Automate里都不需要的啊&…

茶文化推广网站

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 网型站前台&#xff1a;网站个介绍、帮助信总、茶文化、茶叶分享、讨论信总 管理功能&#xff1a; 1、管理网站介绍、帮…

刚做外贸,先做平台好还是独立站好?

作为亚马逊这样的平台卖家&#xff0c;依托平台完善的第三方服务和流量红利&#xff0c;很容易将产品卖到海外。如今&#xff0c;随着平台要求越来越严格&#xff0c;管理政策越来越多变&#xff0c;用户需求也越来越多样化和苛刻&#xff0c;卖家在平台上经营店铺的一些问题正…

[附源码]Python计算机毕业设计SSM基于Web的摄影爱好者交流社区(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Clone使用

实现克隆接口:clone方法是默认是Object的方法 1)这个接口是一个标记性的接口(空接口)&#xff0c;他们内部都没有方法和属性&#xff0c;实现这个接口表示这个对象都可以进行克隆&#xff0c;我们调用Object对象的Object.clone()方法&#xff0c;如果没有实现Cloneable的类对象…

【云原生】k8s声明式资源管理

内容预知 1.资源配置清单的管理 1.1 查看资源配置清单 1.2 修改资源配置清单并应用 离线修改 在线修改 1.3 删除资源配置清单 2.自主编写资源清单 2.1 初步认识资源清单中svc的重要配置项 2.2 手动编写 svc资源配置 3.手动生成模板&#xff0c;再编写资源清单 &#x…

解决虚拟机IP地址无法获取和网络无法连接

首先看一下电脑的end33分配成功没有 ifconfig或者使用ip addr查询都可以 ip addr 目录 1.首先看看你的网络连接模式是不是NAT 2.去主机电脑服务查看这四个服务是否开启 3.查看虚拟机子网ip是否分配了 4.修改我们的网卡配置文件&#xff0c;使用下面命令进入文件 5.重启网…