three.jsのShaderMaterialでは自分でシェーダーを作成できますが、ライテイングを考慮したサンプルを見たことがなかったので試してみました。
まず、three.jsで使われているシェーダーはthree.js/src/renderers/shadersに断片化した形で保存されています。断片化されたコードはWebGLProgram.jsで完全なGLSLコードにまとめられます。WebGLProgram.jsはuniform変数やattribute変数なども自動で追加してくれます。
今回はMeshPhongMaterial
で使用されるmeshphong_vert.glslとmeshphong_frag.glslをもとに、最低限必要なものだけを残すことでライティングを憂慮したShaderMaterialを作成します。
以下のコードはthree.jsのr91で試しています。
Vertexシェーダーは次のようになります。normal
やposition
、modelViewMatrix
などの変数の宣言はthree.jsが自動で追加してくれます。
やっていることは単純に頂点位置を座標変換しているだけです。
varying vec3 vViewPosition;
varying vec3 vNormal;
void main() {
vNormal = normalMatrix * normal;
vViewPosition = -(modelViewMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
Fragmentシェーダーは以下のようになっています。#include
はGLSLの文法ではなくthree.js独自のもので、ShaderChunkの対応したものに置き換えられます。
diffuseColor
に指定した色がマテリアルのベースの色になります。
#include <common>
#include <bsdfs>
#include <lights_pars_begin>
#include <lights_pars_maps>
#include <lights_phong_pars_fragment>
vec3 emissive = vec3(0.0);
vec3 specular = vec3(1.0);
float shininess = 8.0;
void main() {
vec3 diffuseColor = vec3(0.5, 0.5, 1.0);
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
#include <specularmap_fragment>
#include <normal_fragment_begin>
#include <normal_fragment_maps>
#include <lights_phong_fragment>
#include <lights_fragment_begin>
#include <lights_fragment_maps>
#include <lights_fragment_end>
vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + emissive;
gl_FragColor = vec4(outgoingLight, 1.0);
}
ShaderMaterialには以下のようにライトに関するuniforms変数が必要になります。
const material = new THREE.ShaderMaterial({
uniforms: {
ambientLightColor: {value: null},
directionalLights: {value: null},
spotLights: {value: null},
rectAreaLights: {value: null},
pointLights: {value: null},
hemisphereLights: {value: null},
directionalShadowMap: {value: null},
directionalShadowMatrix: {value: null},
spotShadowMap: {value: null},
spotShadowMatrix: {value: null},
pointShadowMap: {value: null},
pointShadowMatrix: {value: null},
},
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent
});
material.lights = true;
これを利用すると次のようにライティングを考慮した上でプロシージャルなテクスチャを生成することができます。
https://aadebdeb.github.io/study-three.js/shadermaterial-with-lighting.html
ソースコード全体は以下のようになっています。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="urf-8">
<title>Sample of ShaderMaterial with lightings</title>
</head>
<body>
<script src="./three.js"></script>
<script id="vertexShader" type="x-shader/x-vertex">
varying vec3 vModelPosition;
varying vec3 vViewPosition;
varying vec3 vNormal;
void main() {
vNormal = normalMatrix * normal;
vModelPosition = (modelMatrix * vec4(position, 1.0)).xyz;
vViewPosition = -(modelViewMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
#include <common>
#include <bsdfs>
#include <lights_pars_begin>
#include <lights_pars_maps>
#include <lights_phong_pars_fragment>
uniform float time;
varying vec3 vModelPosition;
vec3 emissive = vec3(0.0);
vec3 specular = vec3(1.0);
float shininess = 8.0;
void main() {
float d = length(vec3(vModelPosition.xz, 0.0));
vec3 diffuseColor = mix(vec3(0.1, 0.3, 0.05), vec3(0.9, 0.7, 0.2), pow(abs(sin(d * 0.3 - time * 5.0)), 12.0));
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
#include <specularmap_fragment>
#include <normal_fragment_begin>
#include <normal_fragment_maps>
#include <lights_phong_fragment>
#include <lights_fragment_begin>
#include <lights_fragment_maps>
#include <lights_fragment_end>
vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + emissive;
gl_FragColor = vec4(outgoingLight, 1.0);
}
</script>
<script>
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.y = 120;
camera.lookAt(0, 0, 0);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x333333);
const material = new THREE.ShaderMaterial({
uniforms: {
ambientLightColor: {value: null},
directionalLights: {value: null},
spotLights: {value: null},
rectAreaLights: {value: null},
pointLights: {value: null},
hemisphereLights: {value: null},
directionalShadowMap: {value: null},
directionalShadowMatrix: {value: null},
spotShadowMap: {value: null},
spotShadowMatrix: {value: null},
pointShadowMap: {value: null},
pointShadowMatrix: {value: null},
time: {value: null},
},
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('fragmentShader').textContent
});
material.lights = true;
const box = new THREE.Mesh(new THREE.BoxBufferGeometry(20, 30, 20), material);
box.position.set(20, 15, 15);
box.rotation.y = Math.PI / 4.0;
scene.add(box);
const ground = new THREE.Mesh(new THREE.BoxBufferGeometry(100, 300, 100), material);
ground.position.y = -150;
scene.add(ground);
const sphere = new THREE.Mesh(new THREE.SphereBufferGeometry(15, 64, 64), material);
sphere.position.set(-10, 7.5, -25);
scene.add(sphere);
const torus = new THREE.Mesh(new THREE.TorusKnotBufferGeometry(10, 4, 128, 24), material);
torus.position.set(-20, 15, 20);
scene.add(torus);
scene.add(new THREE.AmbientLight(0x666666));
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 1, 1);
scene.add(light);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
let time = 0.0;
animate();
function animate() {
requestAnimationFrame(animate);
time += Math.PI * 2.0 / 300;
material.uniforms.time.value = time;
camera.position.x = 150.0 * Math.cos(time);
camera.position.z = 150.0 * Math.sin(time);
camera.lookAt(0, 0, 0);
camera.updateProjectionMatrix();
renderer.render(scene, camera);
}
</script>
</body>
</html>