three.js(六) 地形法向量生成
上一节采用 分形算法生成地形的高度值, 接着我们需要生成每个顶点的法向量。
three.js 的PlaneGeometry 自带有法向量, 法向量分为两种 即 平面法向量 和 平面每个定点法向量。
因此一个n*n 块组成的平面, 有n*n 个平面法向量, 有4*n*n 个顶点法向量。
这两种法向量区别是, 如果材质的shading属性是THREE.SmoothShading 则采用顶点法向量, 如果不是则采用平面法向量, 平面法向量 导致整个面上的法向量处处相同,所以光照可能不够真实。
平面几何体的顶点数组是(n+1)*(n+1)的长度, 因此其法向量数组长度也应该是(n+1)*(n+1) 才合适, 而如果遍历面 将会产生4*n*n个向量, 如何修正这个问题呢?
平面几何体在绘制的过程中, 由sortFacesByMaterial 函数处理生成几何体组。
首先根据材质对几何体分组,
材质编号_当前材质几何体组编号 作为几何体组的标识。
接着将相应的平面块 压入到对应的几何体组中。
控制每个几何体组的定点个数 小于 65535.
为几何体组编全局id号, 并将几何体组压入到 几何体组的List中
geometry.geometryGroups----->map形式访问几何体组
geometry.geometryGroupList-----> 数组形式访问几何体组
首先构建顶点 法向量 tangent, 颜色, 纹理坐标, 面, 线 等buffer。
接着初始化这些buffers。
接着在setMeshBuffers 中为这些buffer赋值, 根据每个独立的面都有将(n+1)*(n+1)个定点值写入到 4*n*n的顶点数组中去,
用户自己定义的属性,如果按照点绑定,则根据面的数量将(n+1)*(n+1)个值写入到 4*n*n 长度的数组中。
如果按照面绑定则把 n*n 个值 写入到 4*n*n 个长度的数组中。
通过以上我们可以看到,绘制平面的时候, 虽然我们只写了(n+1)*(n+1)个定点值,但是引擎实际扩展到 4*n*n 个值,这样最大化了空间的使用,具有最大的灵活性。
知道了引擎的处理方法,我们构建一个(n+1)*(n+1)的shader属性,默认绑定在顶点上,接着计算向量值并赋值给这个属性就可以了。
材质如下:
var pmat = new THREE.ShaderMaterial({ uniforms:{ texture_grass:{type:'t', value:0, texture:THREE.ImageUtils.loadTexture("grassa512.bmp")}, texture_rock:{type:'t', value:1, texture:THREE.ImageUtils.loadTexture("dirt512.bmp")}, light:{type:'v3', value:new THREE.Vector3()}, maxHeight:{type:'f', value:0}, minHeight:{type:'f', value:1}, }, attributes:{ displacement: {type:'f', value:[]}, vexNormal:{type:'v3', value:[]}, }, vertexShader: document.getElementById("vert").textContent, fragmentShader: document.getElementById("frag").textContent, });
其中vertexNormal 就是逐顶点法向量,当然我们也可以直接修改默认每个面块的法向量或者修改平面法向量这两种方法都不方便,所以还是使用一个额外的属性来处理。
这个属性是v3 类型即对应的THREE数据类型是Vector3, 法向量的生成,对于每一个定点其左右定点连接的向量和上下顶点连接的向量的叉乘, 作为自身的法向量。
var v1 = new THREE.Vector3(); var v2 = new THREE.Vector3(); var distX = 2*3/(WIDTH-1); var distY = 2*3/(HEIGHT-1); var vexNormal = pmat.attributes.vexNormal.value; var vertices = pmesh.geometry.vertices; var lmat = new THREE.LineBasicMaterial({color:0xff0000}); for(var i = 0; i < vertices.length; i++) { var row = ~~(i/WIDTH); var col = i%WIDTH; var left = (col-1+WIDTH)%WIDTH; var right = (col+1)%WIDTH; var up = (row-1+HEIGHT)%HEIGHT; var bottom = (row+1)%HEIGHT; var l = value[row*WIDTH+left]; var r = value[row*WIDTH+right]; v1.set(distX, 0, r-l); var u = value[up*WIDTH+col]; var b = value[bottom*WIDTH+col]; v2.set(0, distY, b-u); v1.crossSelf(v2.clone()).normalize(); vexNormal.push(v1.clone()); var lgeo = new THREE.Geometry(); lgeo.vertices.push(new THREE.Vertex()); lgeo.vertices.push(new THREE.Vertex(v1.clone())); var line = new THREE.Line(lgeo, lmat); line.position.set(vertices[i].position.x, vertices[i].position.y, value[i]); pmesh.add(line); }
这里计算的法向量是属于物体空间的, 在shader中我们需要将其转化成世界坐标, normalMatrix 是 世界视图modelView 矩阵的逆转置, 不能将法向量转化到世界坐标,因此,我们传入一个额外的矩阵, 当前引擎似乎只有mat4 的4*4 的矩阵, 因此我们传入4*4 objectMatrix 的逆转置。
normalWorldMatrix 是 要的矩阵。
var pmat = new THREE.ShaderMaterial({ uniforms:{ texture_grass:{type:'t', value:0, texture:THREE.ImageUtils.loadTexture("grassa512.bmp")}, texture_rock:{type:'t', value:1, texture:THREE.ImageUtils.loadTexture("dirt512.bmp")}, light:{type:'v3', value:new THREE.Vector3()}, normalWorldMatrix:{type:'m4', value:new THREE.Matrix4()}, maxHeight:{type:'f', value:0}, minHeight:{type:'f', value:1}, }, attributes:{ displacement: {type:'f', value:[]}, vexNormal:{type:'v3', value:[]}, }, vertexShader: document.getElementById("vert").textContent, fragmentShader: document.getElementById("frag").textContent, //wireframe:true, });
将平面位置调整之后, updateMatrixWorld 更新平面的世界矩阵, 接着将平面的matrixWorld的逆转置赋值给normalWorldMatrix.
normalWorldmatrix.value.getInverse(pmesh.matrixWorld).transpose();
当然在shader里面我们只使用它的3*3 部分, 先将定点法向扩充成 4维 接着只取其前3维度即可。
nor = (normalWorldMatrix * vec4(vexNormal, 0)).xyz
当然加入法向量的目的是 计算光照, 在平面上方设置一个光源位置 作为uniform传入 light.
lightDir = light-pos;
diffuse = max(dot(normalize(lightDir), nor), 0); 作为系数影响亮度。