2021年01月16日 - WebVR・Three.js
WebGLでポイントライト

WebGLのコードを書いてみると、Three.jsの裏側の仕組みがわかって勉強になります。そこで「WebGLで四角形ポリゴンをアニメーション」に続き「点光源によるライティング-wgld.org」を参考に、WebGLとシェーダでポイントライトを試しました。
WebGLでポイントライト
Three.jsでポイントライトを使用する場合はPointLightを設置するだけですが、WebGLの場合はシェーダでライティングの計算をする必要があります。また、ポイントライトをテストするため、トーラスと球体を生成します。
● 行列演算用ライブラリ
「行列演算とライブラリ-wgld.org」を参考に行列演算用ライブラリを読み込みます。
<script src="js/lib/minMatrix.js"></script> <script src="js/script.js" type="module"></script>
● script.js
//===============================================================
// GLSL
//===============================================================
const vertexShader =`
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mvpMatrix;
uniform mat4 mMatrix;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vColor;
void main(void){
vPosition = (mMatrix * vec4(position,1.0)).xyz;
vNormal = normal;
vColor = color;
gl_Position = mvpMatrix * vec4(position,1.0);
}
`;
const fragmentShader =`
precision mediump float;
//モデル座標変換行列の逆行列
uniform mat4 invMatrix;
//ポイントライトの位置座標
uniform vec3 lightPosition;
//視点ベクトル
uniform vec3 eyeDirection;
//環境光
uniform vec4 ambientColor;
//頂点の位置座標
varying vec3 vPosition;
//頂点の法線情報
varying vec3 vNormal;
//頂点の色情報
varying vec4 vColor;
void main(void){
//ポイントライトのライトベクトル
vec3 lightVec = lightPosition - vPosition;
//モデル座標変換の影響を相殺
vec3 invLight = normalize(invMatrix * vec4(lightVec,0.0)).xyz;
vec3 invEye = normalize(invMatrix * vec4(eyeDirection,0.0)).xyz;
vec3 halfLE = normalize(invLight + invEye);
//拡散光
float diffuse = clamp(dot(vNormal,invLight),0.0,1.0) + 0.2;
//反射光
float specular = pow(clamp(dot(vNormal,halfLE),0.0,1.0),50.0);
//環境光を加え描画色を算出
vec4 destColor = vColor * vec4(vec3(diffuse),1.0) + vec4(vec3(specular),1.0) + ambientColor;
gl_FragColor = destColor;
}
`;
//===============================================================
// Init & Redering
//===============================================================
window.addEventListener('load',function(){
init();
rendering();
});
let canvas,gl;
let m,mMatrix,vMatrix,pMatrix,vpMatrix,mvpMatrix,invMatrix;
let attLocation,attStride;
let uniLocation;
let torusData,tIndex,tVBOList;
let sphereData,sIndex,sVBOList;
let lightPosition,eyeDirection,ambientColor;
let count = 0;
function init(){
canvas = document.getElementById('webgl-canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl = canvas.getContext('webgl');
//深度テストを有効化
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
//カリングを有効化
gl.enable(gl.CULL_FACE);
const vShader = createShader(gl.VERTEX_SHADER,vertexShader);
const fShader = createShader(gl.FRAGMENT_SHADER,fragmentShader);
const prg = createProgram(vShader,fShader);
//attributeLocationの取得
attLocation = [];
attLocation[0] = gl.getAttribLocation(prg,'position');
attLocation[1] = gl.getAttribLocation(prg,'normal');
attLocation[2] = gl.getAttribLocation(prg,'color');
//attributeの要素数
attStride = [];
attStride[0] = 3;
attStride[1] = 3;
attStride[2] = 4;
//トーラスを生成
torusData = torus(64,64,0.5,1.5);
const tPosition = createVbo(torusData.p)
const tNormal = createVbo(torusData.n);
const tColor = createVbo(torusData.c);
tVBOList = [tPosition,tNormal,tColor];
tIndex = createIbo(torusData.i);
//球体を生成
sphereData = sphere(64,64,1.75);
const sPosition = createVbo(sphereData.p);
const sNormal = createVbo(sphereData.n);
const sColor = createVbo(sphereData.c);
sVBOList = [sPosition,sNormal,sColor];
sIndex = createIbo(sphereData.i);
//uniformLocationの取得
uniLocation = [];
uniLocation[0] = gl.getUniformLocation(prg,'mvpMatrix');
uniLocation[1] = gl.getUniformLocation(prg,'mMatrix');
uniLocation[2] = gl.getUniformLocation(prg,'invMatrix');
uniLocation[3] = gl.getUniformLocation(prg,'lightPosition');
uniLocation[4] = gl.getUniformLocation(prg,'eyeDirection');
uniLocation[5] = gl.getUniformLocation(prg,'ambientColor');
//行列の生成
m = new matIV();
mMatrix = m.identity(m.create());
vMatrix = m.identity(m.create());
pMatrix = m.identity(m.create());
vpMatrix = m.identity(m.create());
mvpMatrix = m.identity(m.create());
invMatrix = m.identity(m.create());
//ポイントライトの位置
lightPosition = [0.0,0.0,0.0];
//視点ベクトル
eyeDirection = [0.0,0.0,20.0];
//環境光の色
ambientColor = [0.1,0.1,0.1,1.0];
m.lookAt(eyeDirection,[0,0,0],[0,1,0],vMatrix);
m.perspective(50,canvas.width/canvas.height,0.1,100,pMatrix);
m.multiply(pMatrix,vMatrix,vpMatrix);
}
//アニメーション
function rendering(){
gl.clearColor(0.0,0.0,0.0,1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
count ++;
const rad = (count % 360) * Math.PI / 180;
const tx = Math.cos(rad) * 3.5;
const ty = Math.sin(rad) * 3.5;
const tz = Math.sin(rad) * 3.5;
//トーラスのアニメーション
setAttribute(tVBOList,attLocation,attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,tIndex);
m.identity(mMatrix);
m.translate(mMatrix, [tx,-ty,-tz], mMatrix);
m.rotate(mMatrix,rad,[0,1,1],mMatrix);
m.multiply(vpMatrix,mMatrix,mvpMatrix);
m.inverse(mMatrix,invMatrix);
gl.uniformMatrix4fv(uniLocation[0],false,mvpMatrix);
gl.uniformMatrix4fv(uniLocation[1],false,mMatrix);
gl.uniformMatrix4fv(uniLocation[2],false,invMatrix);
gl.uniform3fv(uniLocation[3],lightPosition);
gl.uniform3fv(uniLocation[4],eyeDirection);
gl.uniform4fv(uniLocation[5],ambientColor);
gl.drawElements(gl.TRIANGLES,torusData.i.length,gl.UNSIGNED_SHORT,0);
//球体のアニメーション
setAttribute(sVBOList,attLocation,attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,sIndex);
m.identity(mMatrix);
m.translate(mMatrix,[-tx,ty,tz],mMatrix);
m.rotate(mMatrix,rad,[1,0,1],mMatrix);
m.multiply(vpMatrix,mMatrix,mvpMatrix);
m.inverse(mMatrix,invMatrix);
gl.uniformMatrix4fv(uniLocation[0],false,mvpMatrix);
gl.uniformMatrix4fv(uniLocation[1],false,mMatrix);
gl.uniformMatrix4fv(uniLocation[2],false,invMatrix);
gl.drawElements(gl.TRIANGLES,sphereData.i.length,gl.UNSIGNED_SHORT,0);
gl.flush();
requestAnimationFrame(rendering);
}
//===============================================================
// Function
//===============================================================
function createShader(shaderType,shaderText){
const shader = gl.createShader(shaderType);
gl.shaderSource(shader,shaderText);
gl.compileShader(shader);
return shader;
}
function createProgram(vs,fs){
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
gl.useProgram(program);
return program;
}
function createVbo(data){
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,vbo);
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(data),gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER,null);
return vbo;
}
function createIbo(data){
const ibo = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,ibo);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Int16Array(data),gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,null);
return ibo;
}
function setAttribute(vbo,attL,attS){
for(let i in vbo){
gl.bindBuffer(gl.ARRAY_BUFFER,vbo[i]);
gl.enableVertexAttribArray(attL[i]);
gl.vertexAttribPointer(attL[i],attS[i],gl.FLOAT,false,0,0);
}
}
//===============================================================
// Model
//===============================================================
//トーラス
function torus(row,column,irad,orad,color){
const pos = [];
const nor = [];
const col = [];
const idx = [];
for(let i = 0; i <= row; i++){
const r = Math.PI * 2 / row * i;
const rr = Math.cos(r);
const ry = Math.sin(r);
for(let ii = 0; ii <= column; ii++){
const tr = Math.PI * 2 / column * ii;
const tx = (rr * irad + orad) * Math.cos(tr);
const ty = ry * irad;
const tz = (rr * irad + orad) * Math.sin(tr);
const rx = rr * Math.cos(tr);
const rz = rr * Math.sin(tr);
let tc;
if(color){
tc = color;
}else{
tc = hsva(360 / column * ii,1,1,1);
}
pos.push(tx,ty,tz);
nor.push(rx,ry,rz);
col.push(tc[0],tc[1],tc[2],tc[3]);
}
}
for(let i = 0; i < row; i++){
for(let ii = 0; ii < column; ii++){
const r = (column + 1) * i + ii;
idx.push(r,r+column+1,r+1);
idx.push(r+column+1,r+column+2,r+1);
}
}
return { p:pos,n:nor,c:col,i:idx };
}
//球体
function sphere(row,column,rad,color){
const pos = [];
const nor = [];
const col = [];
const idx = [];
let r;
for(let i = 0; i <= row; i ++){
r = Math.PI / row * i;
const ry = Math.cos(r);
const rr = Math.sin(r);
for(let ii = 0; ii <= column; ii++){
const tr = Math.PI * 2 / column * ii;
const tx = rr * rad * Math.cos(tr);
const ty = ry * rad;
const tz = rr * rad * Math.sin(tr);
const rx = rr * Math.cos(tr);
const rz = rr * Math.sin(tr);
let tc;
if(color){
tc = color;
}else{
tc = hsva(360 / row * i,1,1,1);
}
pos.push(tx,ty,tz);
nor.push(rx,ry,rz);
col.push(tc[0],tc[1],tc[2],tc[3]);
}
}
r = 0;
for(let i = 0; i < row; i++){
for(let ii = 0; ii < column; ii++){
r = (column + 1) * i + ii;
idx.push(r,r+1,r+column+2);
idx.push(r,r+column+2,r+column+1);
}
}
return { p:pos,n:nor,c:col,i:idx };
}
//HSVからRGBへ変換
function hsva(h,s,v,a){
if(s > 1 || v > 1 || a > 1) return;
const th = h % 360;
const i = Math.floor(th / 60);
const f = th / 60 - i;
const m = v * (1 - s);
const n = v * (1 - s * f);
const k = v * (1 - s * (1 - f));
const color = [];
if(!s > 0 && !s < 0){
color.push(v,v,v,a);
}else{
const r = [v,n,m,m,k,v];
const g = [k,v,v,n,m,m];
const b = [m,m,k,v,v,n];
color.push(r[i],g[i],b[i],a);
}
return color;
}
完成したデモになります。WebGLとシェーダでポイントライトを試しました。

