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); 作為係數影響亮度。