2021年02月13日 - WebVR・Three.js
WebGLでキューブ環境マッピング
「WebGLでトゥーンレンダリング」に続き「キューブ環境マッピング-wgld.org」を参考に、WebGLでキューブ環境マッピングを試してみました。
キューブ環境マッピング
キューブ環境マッピングは、擬似的に周囲環境の映り込みを再現するテクスチャマッピングの手法の一つです。WebGLでは、キューブマップテクスチャを使用してキューブ環境マッピングを行います。
通常のテクスチャはバインドするさい、bindTextureの第一引数にgl.TEXTURE_2Dを設定しますが、キューブマップテクスチャはgl.TEXTURE_CUBE_MAPを設定します。
● テクスチャ画像を用意する
キューブ環境マッピングは名前の通り、立方体の展開図のような6枚のテクスチャ画像を使用します。
下記サイトでパノラマ画像からキューブ環境マッピング用のテクスチャ画像に変換することができます。
変換した画像をフォトショップで6枚の画像に分けます。
WebGLでキューブ環境マッピング
● 行列演算用ライブラリ
「minMatrixb.js リファレンス-wgld.org」を参考に、行列演算用ライブラリを読み込みます。minMatrixb.jsにはトーラスと球体を生成する関数が実装されています。
<script src="js/lib/minMatrixb.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 mMatrix; uniform mat4 mvpMatrix; varying vec3 vPosition; varying vec3 vNormal; varying vec4 vColor; void main(void){ //頂点座標 vPosition = (mMatrix * vec4(position,1.0)).xyz; //法線 vNormal = (mMatrix * vec4(normal,0.0)).xyz; vColor = color; gl_Position = mvpMatrix * vec4(position,1.0); } `; const fragmentShader =` precision mediump float; //カメラの座標 uniform vec3 eyePosition; //テクスチャオブジェクトを取得 uniform samplerCube cubeTexture; uniform bool reflection; varying vec3 vPosition; varying vec3 vNormal; varying vec4 vColor; void main(void){ vec3 ref; if(reflection){ //視線ベクトルの反射ベクトル ref = reflect(vPosition - eyePosition,vNormal); }else{ ref = vNormal; } //キューブマップテクスチャの設定 vec4 envColor = textureCube(cubeTexture,ref); vec4 destColor = vColor * envColor; gl_FragColor = destColor; } `; //=============================================================== // Init & Redering //=============================================================== window.addEventListener('load',function(){ init(); rendering(); }); let canvas,gl; let m,mMatrix,vMatrix,pMatrix,vpMatrix,mvpMatrix; let prg,attLocation,attStride; let uniLocation; let sphereData,sVBOList,sIndex; let torusData,tVBOList,tIndex; let q,qt; let cubeTexture = null; 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); let vShader = createShader(gl.VERTEX_SHADER,vertexShader); let fShader = createShader(gl.FRAGMENT_SHADER,fragmentShader); prg = createProgram(vShader,fShader); attLocation = []; attLocation[0] = gl.getAttribLocation(prg,'position'); attLocation[1] = gl.getAttribLocation(prg,'normal'); attLocation[2] = gl.getAttribLocation(prg,'color'); attStride = []; attStride[0] = 3; attStride[1] = 3; attStride[2] = 4; //球体を生成 sphereData = sphere(64,64,2.5,[1.0,1.0,1.0,1.0]); const sPosition = createVbo(sphereData.p); const sNormal = createVbo(sphereData.n); const sColor = createVbo(sphereData.c); sVBOList = [sPosition,sNormal,sColor]; sIndex = createIbo(sphereData.i); //トーラスを生成 torusData = torus(64,64,1.0,2.0,[1.0,1.0,1.0,1.0]); const tPosition = createVbo(torusData.p); const tNormal = createVbo(torusData.n); const tColor = createVbo(torusData.c); tVBOList = [tPosition,tNormal,tColor]; tIndex = createIbo(torusData.i); uniLocation = []; uniLocation[0] = gl.getUniformLocation(prg,'mMatrix'); uniLocation[1] = gl.getUniformLocation(prg,'mvpMatrix'); uniLocation[2] = gl.getUniformLocation(prg,'eyePosition'); //キューブマップテクスチャ uniLocation[3] = gl.getUniformLocation(prg,'cubeTexture'); uniLocation[4] = gl.getUniformLocation(prg,'reflection'); 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()); q = new qtnIV(); qt = q.identity(q.create()); //キューブマップテクスチャ用画像 const cubeSource = new Array( '../img/texture/posx.jpg', '../img/texture/posy.jpg', '../img/texture/posz.jpg', '../img/texture/negx.jpg', '../img/texture/negy.jpg', '../img/texture/negz.jpg' ); //キューブマップテクスチャ用ターゲット const cubeTarget = new Array( gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, ); //キューブマップテクスチャの生成 createCubeTexture(cubeSource,cubeTarget); window.addEventListener('mousemove',mouseMove); } //アニメーション function rendering(){ count += 0.5; const rad = (count % 360) * Math.PI / 180; const rad2 = ((count + 180) % 360) * Math.PI / 180; gl.clearColor(0.0,0.0,0.0,1.0); gl.clearDepth(1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); const eyePosition = [0.0,0.0,20.0]; const camUp = []; q.toVecIII([0.0,0.0,20.0],qt,eyePosition); q.toVecIII([0.0,1.0,0.0],qt,camUp); m.lookAt(eyePosition,[0,0,0],camUp,vMatrix); m.perspective(50,canvas.width/canvas.height,0.1,100,pMatrix); m.multiply(pMatrix,vMatrix,vpMatrix); if(cubeTexture){ //テクスチャオブジェクトの生成 gl.activeTexture(gl.TEXTURE0); //テクスチャオブジェクトのバイド gl.bindTexture(gl.TEXTURE_CUBE_MAP,cubeTexture); //球体のアニメーション setAttribute(sVBOList,attLocation,attStride); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,sIndex); m.identity(mMatrix); m.rotate(mMatrix,rad,[0,0,1],mMatrix); m.translate(mMatrix,[4.0,0.0,0.0],mMatrix); m.multiply(vpMatrix,mMatrix,mvpMatrix); gl.uniformMatrix4fv(uniLocation[0],false,mMatrix); gl.uniformMatrix4fv(uniLocation[1],false,mvpMatrix); gl.uniform3fv(uniLocation[2],eyePosition); gl.uniform1i(uniLocation[3],0); gl.uniform1i(uniLocation[4],true); gl.drawElements(gl.TRIANGLES,sphereData.i.length,gl.UNSIGNED_SHORT,0); //トーラスのアニメーション setAttribute(tVBOList,attLocation,attStride); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,tIndex); m.identity(mMatrix); m.rotate(mMatrix,rad2,[0,0,1],mMatrix); m.translate(mMatrix,[4.0,0.0,0.0],mMatrix); m.rotate(mMatrix,rad,[1,0,1],mMatrix); m.multiply(vpMatrix,mMatrix,mvpMatrix); gl.uniformMatrix4fv(uniLocation[0],false,mMatrix); gl.uniformMatrix4fv(uniLocation[1],false,mvpMatrix); gl.uniform1i(uniLocation[4],true); gl.drawElements(gl.TRIANGLES,torusData.i.length,gl.UNSIGNED_SHORT,0); } gl.flush(); requestAnimationFrame(rendering); } //=============================================================== // Controll //=============================================================== function mouseMove(event){ const wh = 1 / Math.sqrt(canvas.width * canvas.width + canvas.height * canvas.height); let x = event.clientX - canvas.offsetLeft - canvas.width * 0.5; let y = event.clientY - canvas.offsetTop - canvas.height * 0.5; let sq = Math.sqrt(x * x + y * y); const r = sq * 2.0 * Math.PI * wh; if(sq != 1){ sq = 1 / sq; x *= sq; y *= sq; } if(q){ q.rotate(r,[y,x,0.0],qt); } } //=============================================================== // 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); } } //キューブマップテクスチャを生成 function createCubeTexture(source,target){ const cImg = []; for(let i = 0; i < source.length; i++){ //画像の読み込み cImg[i] = new cubeMapImage(); cImg[i].data.src = source[i]; } function cubeMapImage(){ this.data = new Image(); //画像の読み込みをチェック this.data.onload = function(){ this.imageDataLoaded = true; checkLoaded(); } } //画像の読み込みをチェック function checkLoaded(){ if( cImg[0].data.imageDataLoaded && cImg[1].data.imageDataLoaded && cImg[2].data.imageDataLoaded && cImg[3].data.imageDataLoaded && cImg[4].data.imageDataLoaded && cImg[5].data.imageDataLoaded){ //画像の読み込み完了後、キューブマップテクスチャを生成 generateCubeMap(); } } //キューブマップテクスチャを生成 function generateCubeMap(){ //テクスチャオブジェクトの生成 const tex = gl.createTexture(); //テクスチャオブジェクトをバインド gl.bindTexture(gl.TEXTURE_CUBE_MAP,tex); for(let j = 0; j < source.length; j++){ //画像を設定 gl.texImage2D(target[j],0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,cImg[j].data); } //ミットマップを生成 gl.generateMipmap(gl.TEXTURE_CUBE_MAP); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); cubeTexture = tex; //テクスチャオブジェクトのバインドを無効化 gl.bindTexture(gl.TEXTURE_CUBE_MAP,null); } }
完成したデモになります。WebGLでキューブ環境マッピングを試してみました。