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とシェーダでポイントライトを試してみました。