Twitter
2021年01月16日 - Three.js・WebVR

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

  • このエントリーをはてなブックマークに追加

関連記事

前の記事へ

WebGLで四角形ポリゴンをアニメーション

次の記事へ

WebGLでクォータニオン