three.jsのShaderMaterialまたはRawShaderMaterialを使うと自分で書いたシェーダーでシェーディングを行うことができます。ShaderMaterialを使うと組み込みのuniformsとattributesを使うことができますが、RawShaderMaterialにはそのようなサポートがありません。ShaderMaterialで使用できるuniformsやattributesはここにまとまっています。
今回はShaderMaterialを使用して、シーン内のLight(DirectionalLight、PointLightなど)を考慮したライティングをしたいと思います。
ランバート反射を実装したMeshLambertMaterialはドキュメントに書いてあるように、頂点単位でライティングを行うグーローシェーディングになっています。今回はサンプルとして、ShaderMaterialを使ってピクセル単位でランバート反射モデルによりライティングを行うマテリアルを作ります。
使用するthree.jsのバージョンはr109です。
まずShaderMaterialの基本的な使用方法を確認します。以下はオブジェクトを単色で塗りつぶすだけのシンプルなShaderMaterialです。vertexShader
は頂点シェーダー、fragmentShader
はフラグメントシェーダーのソースコードで、uniforms
はuniforms変数の値になります。projectionMatrix
やmodelViewMatrix
、position
は組み込みuniforms、組み込みattributesで、これらの宣言はthree.js側で挿入されるためソースコードには不要です。
const material = new THREE.ShaderMaterial({
vertexShader: `
void main(void) {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 color;
void main(void) {
gl_FragColor = vec4(color, 1.0);
}
`,
uniforms: {
color: { value: new THREE.Color(0xff0000) },
},
});
基本的な使い方を確認したところで、ShaderMaterialでライティングをしていきます。
まずはShaderMaterialのlightsプロパティにtrueを設定して、レンダリング時にシーン内のライトがuniformsとしてシェーダーに渡るようにします。
const material = new THREE.ShaderMaterial({
...
lights: true,
});
しかしこれだけではShaderMaterialのuniformsプロパティにライトの情報がないため、実行時にエラーになります。既存のマテリアルで使用されるuniformsの情報はShaderLib.jsにまとまっており、例えばMeshLambertMaterialは以下のようになっています。
lambert: {
uniforms: mergeUniforms( [
UniformsLib.common,
UniformsLib.specularmap,
UniformsLib.envmap,
UniformsLib.aomap,
UniformsLib.lightmap,
UniformsLib.emissivemap,
UniformsLib.fog,
UniformsLib.lights,
{
emissive: { value: new Color( 0x000000 ) }
}
] ),
vertexShader: ShaderChunk.meshlambert_vert,
fragmentShader: ShaderChunk.meshlambert_frag
},
これを見るとUniformLib.lights
というものがあり、ここにライト関係のuniformsがありそうです。UniformLib.js内のUniformLib.lights
は次のようになっており、確かにlightsの情報があります。
lights: {
ambientLightColor: { value: [] },
lightProbe: { value: [] },
directionalLights: { value: [], properties: {
direction: {},
color: {},
shadow: {},
shadowBias: {},
shadowRadius: {},
shadowMapSize: {}
} },
directionalShadowMap: { value: [] },
directionalShadowMatrix: { value: [] },
spotLights: { value: [], properties: {
color: {},
position: {},
direction: {},
distance: {},
coneCos: {},
penumbraCos: {},
decay: {},
shadow: {},
shadowBias: {},
shadowRadius: {},
shadowMapSize: {}
} },
spotShadowMap: { value: [] },
spotShadowMatrix: { value: [] },
pointLights: { value: [], properties: {
color: {},
position: {},
decay: {},
distance: {},
shadow: {},
shadowBias: {},
shadowRadius: {},
shadowMapSize: {},
shadowCameraNear: {},
shadowCameraFar: {}
} },
pointShadowMap: { value: [] },
pointShadowMatrix: { value: [] },
hemisphereLights: { value: [], properties: {
direction: {},
skyColor: {},
groundColor: {}
} },
// TODO (abelnation): RectAreaLight BRDF data needs to be moved from example to main src
rectAreaLights: { value: [], properties: {
color: {},
position: {},
width: {},
height: {}
} }
},
ShaderMaterialでもライトのuniformsの使うために、uniforms
プロパティでUniformsLib.lights
を使用するようにします。UniformUtils.merge
はthree.js内コードで使用されているmergeUniforms
メソッドと同じもので、複数のオブジェクトに分かれているuniformsを一つのオブジェクトにまとめてくれます。
const material = new THREE.ShaderMaterial({
...
uniforms: THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
{ ... } // 任意のuniforms
]),
lights: true,
});
uniformsとしてライトの情報をシェーダーで渡せるようになったので、次はシェーダー側のソースコードを書いていきたいと思います。
まずは参考として、MeshLambertMaterialのシェーダーについて確認していきます。頂点シェーダーはmeshlambert_vert.glsl.jsに、フラグメントシェーダーはmeshlambert_frag.glsl.jsにソースコードがあります。three.jsではシェーダーのコードを機能単位で分けており、それを#include
することで一つのシェーダーを生成しています。MeshLambertMaterialはグーローシェーディングなので、ライティングの処理は頂点シェーダーにあることになります。
まずライトの情報の読み込みですが、次のように#include <lights_pars_begin>
(lights_pars_begin.glsl.js)で行われています。
...
uniform vec3 ambientLightColor;
...
uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];
...
uniform PointLight pointLights[ NUM_POINT_LIGHTS ];
...
また、ライティングは次のように#include <lights_pars_begin>
(lights_lambert_vertex.glsl.js)で行われています。
// ソースコードを一部のみ抜粋
...
#if NUM_POINT_LIGHTS > 0
#pragma unroll_loop
for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {
getPointDirectLightIrradiance( pointLights[ i ], geometry, directLight );
dotNL = dot( geometry.normal, directLight.direction );
directLightColor_Diffuse = PI * directLight.color;
vLightFront += saturate( dotNL ) * directLightColor_Diffuse;
#ifdef DOUBLE_SIDED
vLightBack += saturate( -dotNL ) * directLightColor_Diffuse;
#endif
}
#endif
...
以上のことから、フラグメントシェーダーで#include <lights_pars_begin>
によりライトの情報をuniformsで読み込み、main関数内で#include <lights_lambert_vertex>
相当の処理を行えばピクセル単位のランバートシェーディングができそうです。
const material = new THREE.ShaderMaterial({
vertexShader: `...`,
fragmentShader: `
...
#include <lights_pars_begin>
...
void main(void) {
...
// #include <lights_lambert_vertex> 相当の処理
...
}
`,
uniforms: THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
{ ... } // 任意のuniforms
]),
lights: true,
});
というわけで、以上のことを踏まえてShaderMaterialでシーン内のライトを考慮したピクセル単位のランバート反射モデルを実装すると次のようになります。MeshLambertMaterialのcolorプロパティとemissiveプロパティだけを考慮した簡略化バージョンです。
const material = new THREE.ShaderMaterial({
vertexShader: `
varying vec3 vViewPosition;
varying vec3 vNormal;
void main(void) {
vNormal = normalMatrix * normal;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vViewPosition = mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform vec3 diffuse;
uniform vec3 emissive;
varying vec3 vViewPosition;
varying vec3 vNormal;
#include <common>
#include <bsdfs>
#include <lights_pars_begin>
void main(void) {
vec3 mvPosition = vViewPosition;
vec3 transformedNormal = vNormal;
// ref: https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/lights_lambert_vertex.glsl.js
GeometricContext geometry;
geometry.position = mvPosition.xyz;
geometry.normal = normalize(transformedNormal);
geometry.viewDir = (normalize(-mvPosition.xyz));
vec3 lightFront = vec3(0.0);
vec3 indirectFront = vec3(0.0);
IncidentLight directLight;
float dotNL;
vec3 directLightColor_Diffuse;
#if NUM_POINT_LIGHTS > 0
#pragma unroll_loop
for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {
getPointDirectLightIrradiance(pointLights[ i ], geometry, directLight);
dotNL = dot(geometry.normal, directLight.direction);
directLightColor_Diffuse = PI * directLight.color;
lightFront += saturate(dotNL) * directLightColor_Diffuse;
}
#endif
#if NUM_SPOT_LIGHTS > 0
#pragma unroll_loop
for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {
getSpotDirectLightIrradiance(spotLights[ i ], geometry, directLight);
dotNL = dot(geometry.normal, directLight.direction);
directLightColor_Diffuse = PI * directLight.color;
lightFront += saturate(dotNL) * directLightColor_Diffuse;
}
#endif
#if NUM_DIR_LIGHTS > 0
#pragma unroll_loop
for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
getDirectionalDirectLightIrradiance(directionalLights[ i ], geometry, directLight);
dotNL = dot(geometry.normal, directLight.direction);
directLightColor_Diffuse = PI * directLight.color;
lightFront += saturate(dotNL) * directLightColor_Diffuse;
}
#endif
#if NUM_HEMI_LIGHTS > 0
#pragma unroll_loop
for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {
indirectFront += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry );
}
#endif
// ref: https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshlambert_frag.glsl.js
vec4 diffuseColor = vec4(diffuse, 1.0);
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
vec3 totalEmissiveRadiance = emissive;
reflectedLight.indirectDiffuse = getAmbientLightIrradiance(ambientLightColor);
reflectedLight.indirectDiffuse += indirectFront;
reflectedLight.indirectDiffuse *= BRDF_Diffuse_Lambert(diffuseColor.rgb);
reflectedLight.directDiffuse = lightFront;
reflectedLight.directDiffuse *= BRDF_Diffuse_Lambert(diffuseColor.rgb);
vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;
gl_FragColor = vec4(outgoingLight, diffuseColor.a);
}
`,
uniforms: THREE.UniformsUtils.merge([
THREE.UniformsLib.lights,
{
'diffuse': { value: new THREE.Color(0xffffff) },
'emissive': { value: new THREE.Color(0x000000) },
}
]),
lights: true,
});
デモです。
https://aadebdeb.github.io/Sample_Three_ShaderMaterial_Lighting/index.html
(ソースコード: https://github.com/aadebdeb/Sample_Three_ShaderMaterial_Lighting)
手前側がランバート反射モデルを実装したShaderMaterial、奥側がMeshLambertMaterialを使用しています。同じようにシェーディングされていますが、ShaderMaterialのほうがピクセル単位でシェーディングしているので綺麗にレンダリングされています。