效果:
这次主要碰到两个难点:
1. 如何进行切线空间的变换,使得即使 mesh 发生变换,法线也依然是正确的。(图中平面已绕 x 轴旋转90°)
2. 正确计算光照
探索过程
一开始考虑基本上有3种可能:
1. 作为 attribute 引入,这也是 opengl 教程的方法
2. 在 vertex shader 里实现,有博客http://www.zwqxin.com/archives/shaderglsl/review-normal-map-bump-map.html 但是并看不懂
3. 在 frag shader 里实现
https://github.com/mrdoob/three.js/blob/6e89128f1ae239f29f2124a43133bb3d767b19bf/src/renderers/shaders/ShaderChunk/normalmap_pars_fragment.glsl
原理博客:http://hacksoflife.blogspot.com/2009/11/per-pixel-tangent-space-normal-mapping.html
https://github.com/mrdoob/three.js/issues/7094 这个串就是讨论关于 three.js 的 tangent space 的实现问题的,可以看到在约 r70之前是有支持的,但是现在已经去掉了。
一开始是觉得按 attribute 的方法实现好像太累赘了,如果要做的话(按 learn-opengl 教程的风格),应该得取 geometry 的 face 信息,然后计算,然后新建一个 bufferGeometry 放入自定义 attribute。
不过经过这两天的研究,发现 three.js 用的是第三种办法,用 GLSL内置的函数dFdy
和dFdx
在片段着色器中计算TBN矩阵
。
一开始我用的就是这段代码:
// Per-Pixel Tangent Space Normal Mapping
// http://hacksoflife.blogspot.ch/2009/11/per-pixel-tangent-space-normal-mapping.html
vec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm ) {
// Workaround for Adreno 3XX dFd*( vec3 ) bug. See #9988
vec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );
vec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );
vec2 st0 = dFdx( vUv.st );
vec2 st1 = dFdy( vUv.st );
float scale = sign( st1.t * st0.s - st0.t * st1.s ); // we do not care about the magnitude
vec3 S = normalize( ( q0 * st1.t - q1 * st0.t ) * scale );
vec3 T = normalize( ( - q0 * st1.s + q1 * st0.s ) * scale );
vec3 N = normalize( surf_norm );
mat3 tsn = mat3( S, T, N );
vec3 mapN = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;
mapN.xy *= normalScale;
mapN.xy *= ( float( gl_FrontFacing ) * 2.0 - 1.0 );
return normalize( tsn * mapN );
}
通过搜索 github 仓库,调用这个函数的代码是在
https://github.com/mrdoob/three.js/blob/6e89128f1ae239f29f2124a43133bb3d767b19bf/src/renderers/shaders/ShaderChunk/normal_fragment_maps.glsl#L23
normal = perturbNormal2Arb( -vViewPosition, normal );
而vViewPosition = -modelViewPosition
// vertex shader
modelViewPosition = modelViewMatrix * vec4(position,