LoginSignup
11
12

More than 3 years have passed since last update.

three.jsのShaderMaterialでシーン内のライトを使用する

Posted at

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変数の値になります。projectionMatrixmodelViewMatrixpositionは組み込み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は以下のようになっています。

/src/renderers/shaders/ShaderLib.js
    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の情報があります。

/src/renderers/shaders/UniformsLib.js
    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)で行われています。

/src/renderers/shaders/ShaderChunk/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)で行われています。

/src/renderers/shaders/ShaderChunk/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_lighting.png
手前側がランバート反射モデルを実装したShaderMaterial、奥側がMeshLambertMaterialを使用しています。同じようにシェーディングされていますが、ShaderMaterialのほうがピクセル単位でシェーディングしているので綺麗にレンダリングされています。

11
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
12