webgl实现法线贴图

法线纹理贴图的作用

法线贴图是一个存储在纹理中的法线向量图,用于在着色过程中替代实际几何体的法线。它通过改变每个像素的法线来影响光照计算,从而模拟复杂的表面细节。存储法线的纹理,会如下图所示:

图片来自网络
图片来自网络

纹理贴图的实现原理

要想实现法线纹理贴图,要引入切线空间,那什么是切线空间呢。

2.1切线空间的定义

切线空间是相对于三维模型表面上的每个顶点或像素定义的局部坐标系,它提供了一种将法线贴图数据转换到模型表面局部坐标系的方式,说白了就是改变了原来模型的法线方向,从而改变了,光照射时的反射方向。

2.2切线空间的公式

切线空间的坐标系,有三个部分组成:

  • 切线向量(Tangent, T)
  • 副切线向量(Bitangent, B)
  • 法线向量(Normal, N)

接下来就是切线空间的求法了

图片来自网络
图片来自网络

根据上面的定义可以通过js代码进行实现如下:

/**
 * 计算三角形顶点的切线和副切线
 * @param {Vector3} p0 - 第一个顶点的坐标
 * @param {Vector3} p1 - 第二个顶点的坐标
 * @param {Vector3} p2 - 第三个顶点的坐标
 * @param {Vector2} uv0 - 第一个顶点的UV坐标
 * @param {Vector2} uv1 - 第二个顶点的UV坐标
 * @param {Vector2} uv2 - 第三个顶点的UV坐标
 * @returns {[Vector3, Vector3]} - 切线和副切线向量
 */
function getTBNTriangle(p0, p1, p2, uv0, uv1, uv2) {
  const e1 = p1.clone().sub(p0); // 计算第一个边向量
  const e2 = p2.clone().sub(p0); // 计算第二个边向量

  const dUV1 = uv1.clone().sub(uv0); // 计算第一个UV边向量
  const dUV2 = uv2.clone().sub(uv0); // 计算第二个UV边向量

  const f = 1.0 / (dUV1.x * dUV2.y - dUV2.x * dUV1.y); // 计算系数

  // 计算切线向量
  const tangent = new Vector3(
    f * (dUV2.y * e1.x - dUV1.y * e2.x),
    f * (dUV2.y * e1.y - dUV1.y * e2.y),
    f * (dUV2.y * e1.z - dUV1.y * e2.z)
  ).normalize();

  // 计算副切线向量
  const bitangent = new Vector3(
    f * (-dUV2.x * e1.x + dUV1.x * e2.x),
    f * (-dUV2.x * e1.y + dUV1.x * e2.y),
    f * (-dUV2.x * e1.z + dUV1.x * e2.z)
  ).normalize();

  return [tangent, bitangent];
}

要注意的是当循环遍历所有三角的定义时,会有几个面共用顶点的现象,要进行累加平均。当然,我们也可以不用通过js计算,可以通过webgl内置函数进行实现,在webgl2中有内置函数dFdx和dFdy这两个求偏导数函数进行计算切线空间。

纹理贴图实现的代码

顶点着色器
const v = `#version 300 es
precision mediump float;
in vec3 a_position;
in vec3 a_normal;
in vec2 a_uv;
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_project;
uniform mat4 u_normal;
out vec3 vNormal;
out vec3 vPos;
out vec2 vUv;


void main() {
  vNormal = (u_normal * vec4(a_normal,1.0)).xyz;
  vPos = a_position;
  vUv = a_uv;
  vec4 position = u_project * u_view * u_model * vec4(a_position, 1.0);
  gl_Position = position;
}
`
片元着色器
const f = `#version 300 es
precision mediump float;
uniform vec3 kd;
uniform vec3 am;
uniform vec3 eye;
uniform vec3 ks;
uniform float p;
uniform float intensity;
uniform vec3 light;
uniform sampler2D tMap;
in vec3 vNormal;
in vec3 vPos;
in vec2 vUv;
out vec4 FragColor;

vec3 getNormal() {
  // 计算片段位置在 x 方向上的变化率
  vec3 pos_dx = dFdx(vPos.xyz);
  // 计算片段位置在 y 方向上的变化率
  vec3 pos_dy = dFdy(vPos.xyz);
  // 计算纹理坐标在 x 方向上的变化率
  vec2 tex_dx = dFdx(vUv);
  // 计算纹理坐标在 y 方向上的变化率
  vec2 tex_dy = dFdy(vUv);

  // 计算切线向量 t,使用导数变化率
  vec3 t = normalize(pos_dx * tex_dy.t - pos_dy * tex_dx.t);
  // 计算副切线向量 b,使用导数变化率
  vec3 b = normalize(-pos_dx * tex_dy.s + pos_dy * tex_dx.s);
  // 构建 TBN 矩阵(切线、副切线、法线),用于将法线从切线空间转换到世界空间
  mat3 tbn = mat3(t, b, normalize(vNormal));

  // 从法线贴图中获取法线,并将其值映射到 [-1, 1] 区间
  vec3 n = texture(tMap, vUv).rgb * 2.0 - 1.0;
  // 将法线从切线空间转换到世界空间,并归一化
  return normalize(tbn * n);
}

void main() {
  vec3 dir = normalize(eye - vPos);
  vec3 lightDir = normalize(light - vPos);
  float dist = length(light - vPos);
  vec3 halfDir = normalize((lightDir + dir) / 2.0);
  vec3 normal = getNormal();
  vec3 color = am + kd * (intensity / dist) * max(0.0, dot(lightDir, normal)) + ks * (intensity / dist) * pow(max(0.0, dot(halfDir, normal)), p);
  FragColor = vec4(pow(color, vec3(1.0 / 2.2)), 1.0);
}
`

纹理贴图实现的效果

结论

从上面的图片,可以明显看出明亮阴暗的效果。

  • 12
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值