法线贴图中的法线向量定义在切线空间中,在切线空间中,法线永远指着正z方向。切线空间是位于三角形表面之上的空间:法线相对于单个三角形的本地参考框架。它就像法线贴图向量的本地空间;它们都被定义为指向正z方向,无论最终变换到什么方向。使用一个特定的矩阵我们就能将本地/切线空间中的法线向量转成世界或视图空间下,使它们转向到最终的贴图表面的方向。

摘自 法线贴图 - LearnOpenGL CN

一、three.js获取切线空间

新版three.js 的BufferGeometryUtils.js 删除了computeTangents方法,以computeMikkTSpaceTangents 替代

computeMikkTSpaceTangents 参数为( BuffGeoMetry , MikkTSpace , negateSign )

  • negateSign -- 是否对每个切线的符号分量 (.w) 取反。某些格式的法线贴图约定需要,包括 glTF。

返回切线数据(vec4)

mikktspace 为 three/examples/jsm/libs/mikktspace.module.js (旧版没有,可以npm引入)

使用computeMikkTSpaceTangents前需要等MikkTSpace.ready完成

二、使用步骤

1.初始化initMikkTSpace

代码如下:

import { computeMikkTSpaceTangents } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import { wasm,isReady,ready,generateTangents } from 'three/examples/jsm/libs/mikktspace.module.js'
async initMikkTSpace(cb){
      await ready
const geo01 = new THREE.SphereGeometry(6, 64, 32)
      // const geo01 = new THREE.BoxGeometry( 6, 6, 6 );
      this.initMikkTSpace(cb=>{
        let MikkTSpace = {
          wasm:wasm,
          isReady:isReady,
          generateTangents:generateTangents
        computeMikkTSpaceTangents(geo01,MikkTSpace)

2.编辑材质

getShadowMask()返回的是阴影

主要步骤:1.attribute vec4 tangent;是computeMikkTSpaceTangents直接添加到geo的切线数据

vec3 nowPoint = vec3((modelViewMatrix * vec4( position, 1.0 )).xyz);

lightToPos = myLight - nowPoint;

获取光与点的向量

vNormal = normalize(normalMatrix * normal);//获取法线

          vec3 vTangent = normalize( normalMatrix * tangent.xyz );//切线

          vec3 vBinormal = normalize(cross( vNormal, vTangent ) * tangent.w);//副切线

          tbn = mat3(vTangent, vBinormal, vNormal);//切线空间

代码如下:

initShader(tangent,myLight){
      let textureLoader = new THREE.TextureLoader();
      let vertexShader = `
      varying vec3 vNormal;
      varying vec2 vUv;
      attribute vec4 tangent;
      varying vec4 vtangent;
      varying mat3 tbn;
      uniform vec3 myLight;
      varying vec3 lightToPos;
      ${THREE.ShaderChunk[ "common" ]}
      ${THREE.ShaderChunk[ "bsdfs" ]}
      ${THREE.ShaderChunk[ "shadowmap_pars_vertex" ]}
      void main()
          ${THREE.ShaderChunk['beginnormal_vertex']}
          ${THREE.ShaderChunk['defaultnormal_vertex']}
          ${THREE.ShaderChunk[ "begin_vertex" ]}
          ${THREE.ShaderChunk[ "project_vertex" ]}
          ${THREE.ShaderChunk[ "worldpos_vertex" ]}
          ${THREE.ShaderChunk[ "shadowmap_vertex" ]}
          vec3 nowPoint = vec3((modelViewMatrix * vec4( position, 1.0 )).xyz);
          lightToPos = myLight - nowPoint;
          vNormal = normalize(normalMatrix * normal);
          vec3 vTangent = normalize( normalMatrix * tangent.xyz );
          vec3 vBinormal = normalize(cross( vNormal, vTangent ) * tangent.w);
          tbn = mat3(vTangent, vBinormal, vNormal);
          vUv = uv;
          vtangent = tangent;
          gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
      let fragmentShader = `
      // uniform vec3 light;
      varying vec3 vNormal;
      varying vec3 lightToPos;
      varying vec2 vUv;
      varying vec4 vtangent;
      uniform sampler2D u_texture;
      uniform sampler2D u_textureNormal;
      varying mat3 tbn;
      ${THREE.ShaderChunk[ "common" ]}
      ${THREE.ShaderChunk[ "packing" ]}
      ${THREE.ShaderChunk[ "bsdfs" ]}
      ${THREE.ShaderChunk[ "lights_pars_begin" ]}
      ${THREE.ShaderChunk[ "shadowmap_pars_fragment" ]}
      ${THREE.ShaderChunk[ "shadowmask_pars_fragment" ]}
      //获取纹理颜色
      vec3 mygetPixelColor(sampler2D mytexture) {
				return texture2D(mytexture, vUv).rgb;
      void main(){
          vec3 normalColor = mygetPixelColor(u_textureNormal);
          normalColor = normalColor * 2.0 - 1.0;
          vec3 anynormalColor = normalize(tbn * normalColor);
          //处理光照
          float diff = max( dot(anynormalColor , normalize(lightToPos)) , 0.0 );
          vec3 diffuse = diff * vec3(1,1,1);//diff * lightColor
          vec3 textureColor = mygetPixelColor(u_texture);
          vec3 addDiffuse = textureColor + diffuse;
          vec3 shadowColor = vec3(0,0,0);
          vec3 addShadow = mix( shadowColor , addDiffuse ,getShadowMask());
         	gl_FragColor = vec4(addShadow,1.0);
      //着色器材质
      let sm = new THREE.ShaderMaterial({
          uniforms: THREE.UniformsUtils.merge( [
            THREE.UniformsLib[ "lights" ],
              opacity:  { type: 'f', value: 1.0 },
              // tangent:  { value: tangent },
              myLight:  { value: myLight.position},
              u_texture:{value:textureLoader.load(require('../../assets/brickwall.jpg'))},
              u_textureNormal:{value:textureLoader.load(require('../../assets/brickwall_normal.jpg'))},
          vertexShader: vertexShader,
          fragmentShader: fragmentShader,
          side: THREE.FrontSide,
          lights: true
      return sm

3.阴影处理

renderer.shadowMap.enabled = true;
let light = new THREE.DirectionalLight(0xffffff);
      light.position.set(42,60,0);
      //告诉平行光需要开启阴影投射
      light.castShadow = true;
      light.shadow.mapSize.width = 1024; // default 512
      light.shadow.mapSize.height = 1024; // default 512
      //阴影相机范围
      light.shadow.camera.near = 0.5; // default 0.5
      light.shadow.camera.far = 100; // default 500
      light.shadow.camera.left = -30
      light.shadow.camera.right = 30
      light.shadow.camera.top = 30
      light.shadow.camera.bottom = -30
      scene.add(light);
mikktspace-wasm Web程序顶点切线计算。 关于切线法线贴图的一个常见误解是,这种表示某种程度上与资产无关。 但是,从高分辨率表面采样/捕获然后转换为切线空间的法线更像是编码。 因此,为了反转法线的原始捕获场,用于解码的变换必须与用于编码的变换完全相反。 —莫滕·S·米克森 当法线贴图渲染不正确,变形或意外倒置的插入和拉伸时,可能是造成这种误解的原因。 大多数法线贴图贝克使用MikkTSpace标准在创建法线贴图时生成顶点切线,而glTF 2.0规范建议使用该技术。 当未提供原始切线时,在运行时重建切线空间的引擎通常会使用其他方法(例如,像素着色器中的导数)来提高效率。 这对于大多数资产而言效果很好,但对其他资产可能效果不佳。 如果你有... 需要在许多引擎中可预测地呈现的资产 需要生产具有可预测的法线图行为的资产的资产管道 需要完美支持任意资产的引擎(并且可以承受 mikktspace Mikkelsen切线空间算法参考实现的端口。 至少需要Rust 1.31.0。 示例生成了Mikkelsen切线空间算法参考实现的mikktspace端口的演示。 至少需要Rust 1.31.0。 示例生成演示为每边具有4个三角形面的立方体生成切线。 货物运行-示例生成许可证协议,该协议根据Apache许可证2.0版(LICENSE-APACHE或http://www.apache.org/licenses/LICENSE-2.0)许可获得MIT许可证(LICENSE-MIT或http:// opensource .org / licenses / MIT)供您选择。 除非您明确声明其他 一、阴影的由来 Three.js 中使用 Shadow Map 的技术来产生阴影。 Shadow Map(阴影贴图)是一种使用深度纹理来为渲染阴影提供解决方案的多通道计算,用投射光源代替最终视口来观察场景。通过移动视口到光源位置,可以观察到这个位置每个东西都是明亮的,因为从光的角度来看是没有阴影的。 从光源的角度将场景的深度渲染到一张深度缓冲区中,我们可以在场景中获得一张阴影或者无阴影的贴图,一张阴影贴图。 因此在使用Three.js中的光源时可以设置其 shadow map 的分辨率,分辨率越高,阴影质 每多一个能投射阴影光源,就会重复绘制一次场景。阴影的计算是一个光源一个光源的依次计算的。 要展示阴影需要,渲染器开启阴影渲染、灯光投开启投射阴影、物体开启接收阴影 只有DirectionalLight、PointLight、RectAreaLight、SpotLight这4种光源才能投谁阴影。 MeshLambertMaterial材质是不能接受投射阴影的。 shader graphA recent Unity Labs paper introduces a new framework for blending normal maps that is easy and intuitive for both technical artists and graphics engineers. This approach overcomes several ... 摘抄“GPU Programming And Cg Language Primer 1rd Edition” 中文名“GPU编程与CG语言之阳春白雪下里巴人”    投影纹理映射(Projective Texture Mapping)最初由Segal在文章“Fast shadows and lighting effects using texture three.js加载纹理View demo 查看演示Download Source 下载源Texture projection is a way of mapping a texture onto a 3D object and making it look like it was projected from a single point. Think o... three.js 交互View demo 查看演示Download Source 下载源Ever had a personal website dedicated to your work and wondered if you should include a photo of yourself in there somewhere? I recently...