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

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

说明

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




什么是法线贴图?

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





什么是切线空间?


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

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


325a343991d341c08a11a195ba15e82f.png



为什么需要切线空间?

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


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


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





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


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


caffb87cd7034b599e3a8942bd7963af.png


公式如下:

57a5bc2fd1ae4c1f8d3da6b8cf3989b2.png


通过 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 的。

4340e9f2a4284e2486f40bb5111d1f60.png



可以参考这篇: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 矩阵左乘从法线纹理中提取出的值,就可以计算出对应的法向量。不过计算偏导数也有一定的性能开销。





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

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


34502fc7c14e441a8cb86cf4ce4c1ded.png


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

<!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>

image.png



法线贴图的应用

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

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

<!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>


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

image.png

目录
相关文章
|
机器学习/深度学习 传感器 算法
【交互式阈值二进制图像】采用彩色或单色图像通过交互/手动方式阈值单色图像或彩色图像的单个色带研究(Matlab代码实现)
【交互式阈值二进制图像】采用彩色或单色图像通过交互/手动方式阈值单色图像或彩色图像的单个色带研究(Matlab代码实现)
|
1月前
|
人工智能 小程序 前端开发
【一步步开发AI运动小程序】六、人体骨骼图绘制
随着AI技术的发展,阿里体育等公司推出的AI运动APP如“乐动力”、“天天跳绳”等,使云上运动会、线上健身等概念广受欢迎。本文将引导您从零开始,利用“云智AI运动识别小程序插件”,在小程序中实现类似功能,包括人体骨骼图的绘制原理及其实现代码,确保骨骼图与人体图像精准重合。下篇将继续介绍运动分析方法。
|
4月前
|
API
【threejs教程】让你的场景更加真实:灯光对物体的影响
【8月更文挑战第6天】threejs教程:让你的场景更加真实,灯光对物体的影响
214 6
【threejs教程】让你的场景更加真实:灯光对物体的影响
|
4月前
|
图形学
小功能⭐️Unity 如何判断物体是否在摄像机视野内或外
小功能⭐️Unity 如何判断物体是否在摄像机视野内或外
|
编解码 图形学 容器
3D模型如何添加表面贴图?
通过模型表面贴图技术,可以实现各种效果,如木纹、石纹、金属反射、布料纹理等,从而使模型更加生动、具有细节和丰富的外观。
166 0
|
存储 数据可视化 索引
校正图像亮度不均匀问题并分析前景对象
校正图像亮度不均匀问题并分析前景对象
119 0
|
自然语言处理 搜索推荐 算法
人脸神经辐射场的掩码编辑方法NeRFFaceEditing,不会三维建模也能编辑立体人脸
人脸神经辐射场的掩码编辑方法NeRFFaceEditing,不会三维建模也能编辑立体人脸
172 0
|
传感器 JSON 数据可视化
【视觉高级篇】22 # 如何用仿射变换来移动和旋转3D物体?
【视觉高级篇】22 # 如何用仿射变换来移动和旋转3D物体?
212 0
【视觉高级篇】22 # 如何用仿射变换来移动和旋转3D物体?
|
数据可视化 API
【视觉高级篇】21 # 如何添加相机,用透视原理对物体进行投影?
【视觉高级篇】21 # 如何添加相机,用透视原理对物体进行投影?
204 0
【视觉高级篇】21 # 如何添加相机,用透视原理对物体进行投影?
|
数据可视化
【视觉基础篇】15 # 如何用极坐标系绘制有趣图案?
【视觉基础篇】15 # 如何用极坐标系绘制有趣图案?
177 0
【视觉基础篇】15 # 如何用极坐标系绘制有趣图案?