WebGL凹凸贴图

什么是凹凸贴图?

凹凸贴图也称法线贴图(Normal Map)

当一个物体表面是凹凸不平的时候,为了使表面在光照下显示更真实有二种方法:1,把模型划分更多的三角形;2:采用法线贴图。更多的三角形代表需要消耗更多算力。法线贴图是,配合低精确的网格(节省算力)也能保证物体表面的质感的一种手段:


精度网格.png

上图中左边有4M个三角形,右边减少到500个三角形配合法线贴图,展现效果差不多。

法线贴图样例

样列.png

法线贴图一般都是像上例这样偏蓝的颜色,因为法线贴图定义为:纹理空中的法线方向的扰动,所有法线的指向都偏向z轴(0, 0, 1),只有砖块的边缘发生偏移比较多。

法线贴图使用

Phong光照计算(https://learnopengl-cn.github.io/02%20Lighting/02%20Basic%20Lighting/)需要使用3个向量(光照方向、视角方向、表面法线),三角形表面的法线是平坦的(三角形顶点线的插值),所以采用法线贴图的法线向量来计算光线,提升效果。

法线.png

切线空间

法线贴图中的法线是定义在纹理空间(切线空间)中的向量,而光照、视角 是在世界坐标系统中,所以需要一个叫TBN的矩阵转化为同一个坐标系中。
TBN矩阵(Tangent-Bitangent-Normal) 是将法向量从贴图变换到物体本地坐标空间的变换矩阵
面它的转置矩阵(称为NBT)就是从物体本地坐标空间转换为纹理坐标系的矩阵。
三角形的每个顶点都有物体空间坐标(x,y,z) 纹理坐标(st,sy),它们之间定义了转换关系,TBN矩阵推导过程参考(https://zhuanlan.zhihu.com/p/150570773

凹凸贴图实现过程(部份代码)

一、计算切线向量
从模型文件加载后,生成了顶点缓冲、顶点索引缓冲

// 遍历顶点索引,每3个索引是一个角形
for (let i = group.indicesOffset; i < group.indicesLength; i += 3) {
    // 取到三个顶点
    let ver0 = scene.verts[scene.indices[i]];
    let ver1 = scene.verts[scene.indices[i + 1]];
    let ver2 = scene.verts[scene.indices[i + 2]];

    // 计算二条边
    let Edge1 = new Vec3(ver1.Position.clone().subtract(ver0.Position))
    let Edge2 = new Vec3(ver2.Position.clone().subtract(ver0.Position))
     
    let DeltaU1 = ver1.TextureCoord.x - ver0.TextureCoord.x
    let DeltaV1 = ver1.TextureCoord.y - ver0.TextureCoord.y
    let DeltaU2 = ver2.TextureCoord.x - ver0.TextureCoord.x
    let DeltaV2 = ver2.TextureCoord.y - ver0.TextureCoord.y

    let f = 1.0 / (DeltaU1 * DeltaV2 - DeltaU2 * DeltaV1)

    let Tagent: Vec3 = new Vec3([0, 0, 0])
    Tagent.x = f * (DeltaV2 * Edge1.x - DeltaV1 * Edge2.x)
    Tagent.y = f * (DeltaV2 * Edge1.y - DeltaV1 * Edge2.y)
    Tagent.z = f * (DeltaV2 * Edge1.z - DeltaV1 * Edge2.z)
    
    ver0.Tangent = <Vec3>ver0.Tangent.addition(Tagent)
    ver1.Tangent = <Vec3>ver1.Tangent.addition(Tagent)
    ver2.Tangent = <Vec3>ver2.Tangent.addition(Tagent)
}

// 切线向量单位化(因为公用顶点重复加了切线,为样实现平均化) 
for (let i = group.indicesOffset; i < group.indicesLength; i++) {
    let ver0 = scene.verts[scene.indices[i]];
    ver0.Tangent = <Vec3>ver0.Tangent.normalize()
}

顶点着色器中计算TBN,关转换相关向量到纹理空间

shader_vs: `# version 300 es
    
    layout (location = 0) in vec3 aPosition;
    layout (location = 1) in vec2 aTexCoord;
    layout (location = 2) in vec3 aNormal;
    layout (location = 3) in vec3 aTangent;  //切线
   
    uniform mat4 uModelMatrix;
    uniform mat3 uNormalMatrix;    //法线矩阵 [模型矩阵左上角3x3部分的逆矩阵的转置矩阵]
    uniform mat4 lightSpaceMatrix;  //光线矩阵

    out vec2 vTexCoord;

    out vec3 vTangentFragPos;             //切线空间的顶点坐标
    out vec3 vTangentViewPos;             //切线空间的观察位置
    out vec3 vTangentDirLightDirection;   //平行光方向
    out vec3 vTangentPointLightPos;       //点光线位置
    out vec3 vTangentFlashLightPos;        // 聚光源位置
    out vec3 vTangentFlashLightDirection; //聚光源方向

    void main(void) {
        gl_Position = project * view * uModelMatrix * vec4(aPosition, 1.0);
        vTexCoord = aTexCoord;
        
        vec3 N = normalize(uNormalMatrix * aNormal);
        vec3 T = normalize(uNormalMatrix * aTangent);
        T = normalize(T - dot(T, N) * N);                         //Gram-Schmidt正交化
        vec3 B = normalize(cross(T, N));

        mat3 NTB = transpose(mat3(T, B, N));   //世界坐标,变换到切线坐标

        vTangentFragPos = NTB * vec3(uModelMatrix * vec4(aPosition, 1.0));
        vTangentViewPos = NTB * viewPos;
        vTangentDirLightDirection = NTB * uDirLight.direction;
        vTangentPointLightPos = NTB * uPointLights.position;
        vTangentFlashLightPos = NTB * uFlashLight.position;
        vTangentFlashLightDirection = NTB * uFlashLight.direction;
}

片元着色器中采样法线向量

shader_fs_blinn: `# version 300 es
        #ifdef GL_ES
            precision highp float;
        #endif
        
        // 材质
        struct Material {
            sampler2D diffuseSampler;  // 纹理采样1(漫反射)
            sampler2D specularSampler; // 纹理采样2(镜面反射)
            sampler2D normalSampler;   // 纹理采样3(法线贴图)

            float shininess;    // 镜面指数 (越大,越完美反射)
            vec3 ambient;       // 材质阴影色,
            vec3 diffuse;       // 材质颜色本色
            vec3 specular;      // 材质光泽度(高光)
            bool useDiffuseTexture;  // 是否使用(漫反射)贴图
            bool useSpecularTexture; // 是否使用(镜面反射)贴图  
            bool useNormalTexture;   // 是否使用(法线)贴图
        };
        in vec2 vTexCoord;

        in vec3 vTangentFragPos;             //切线空间的顶点坐标
        in vec3 vTangentViewPos;             //切线空间的观察位置
        in vec3 vTangentDirLightDirection;   //
        in vec3 vTangentPointLightPos;
        in vec3 vTangentFlashLightPos;
        in vec3 vTangentFlashLightDirection; //

        // 物体材质
        uniform Material uMaterial;
       
        out vec4 FragColor;

        // function declare
        vec3 affactDirLight(DirLight dirLight, vec3 normal, vec3 viewDir, vec2 texCoord);
        vec3 affactPointLight(PointLight pointLight, vec3 normal, vec3 fragPos, vec3 viewDir, vec2 texCoord);
        vec3 affactFlashLight(FlashLight flashLight, vec3 normal, vec3 fragPos, vec3 viewDir, vec2 texCoord);

        void main(void) {
            // 观察者方向 顶点->观察者
            vec3 viewDir = normalize(vTangentViewPos - vTangentFragPos );

            float alpha = 1.0;
            if (uMaterial.useDiffuseTexture) 
                alpha = texture(uMaterial.diffuseSampler,  vTexCoord).w;

            // 法线贴图-采样法线
            vec3 normal = texture(uMaterial.normalSampler, vTexCoord).rgb;
            // 变换范围到[-1,1]
            normal = normalize( normal * 2.0 - 1.0);

            // step-0 计算环境光
            vec3 result = uGlobalAmbient * uMaterial.diffuse;
            if (uMaterial.useDiffuseTexture)
                result = uGlobalAmbient * vec3(texture(uMaterial.diffuseSampler,  vTexCoord));

            // step1 平行光
            if (uHasDirLight)
                result += affactDirLight(uDirLight, normal, viewDir, vTexCoord);

            FragColor = vec4(result, alpha);
        }

        // 计算平行光
        vec3 affactDirLight(DirLight dirLight, vec3 normal, vec3 viewDir, vec2 texCoord) {
            // 指向光源方向
            vec3 lightDir = - vTangentDirLightDirection;
        
            vec3 ambient  = dirLight.ambient  * uMaterial.diffuse;
            if (uMaterial.useDiffuseTexture)
            {
                vec4 text = texture(uMaterial.diffuseSampler,  texCoord);
                ambient = dirLight.ambient * vec3(text);
            }

            // 计算漫反射
            float diff = max(dot(lightDir, normal), 0.0);
            //return vec3(lightDir); 
        
            vec3 diffUse =  dirLight.diffuse * diff * uMaterial.diffuse;
            if (uMaterial.useDiffuseTexture)
            {
                vec4 text = texture(uMaterial.diffuseSampler,  texCoord);
                diffUse = dirLight.diffuse * diff * vec3(text);
            }
        
            // 计算镜面反射
            // 半程向量
            vec3 halfwayDir = normalize(lightDir + viewDir); 
             
            // 计算反射光
            float spec = pow(max(dot(normal, halfwayDir), 0.0), uMaterial.shininess * 4.0);   //Blinng 选择冯氏着色时反光度分量的2到4倍
            vec3 specular = dirLight.specular * spec * uMaterial.specular;
            if (uMaterial.useSpecularTexture)
               specular = dirLight.specular * spec * vec3(texture(uMaterial.specularSampler, texCoord));
        
            return ambient + diffUse + specular;
        }

实现效果对比

如下面二张图,使用法线贴图光泽度更好、细节更加逼真。


使用法线贴图

未使用法线贴图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容