创建一个遵循日光源的日/夜着色器

时间:2019-07-10 19:49:35

标签: javascript three.js shader

我有一个被DirectionalLight照亮的球体,以模仿照射在地球上的太阳。我正在尝试添加一个着色器,该着色器将在晚上在地球的未光照部分显示地球,而在白天则显示光照部分的地球。我计划最终使DirectionalLight在全球范围内旋转,更新着色器以显示当前阴影中的地球部分。我遇到了以下代码笔,它可以部分满足我的需求:https://codepen.io/acauamontiel/pen/yvJoVv

在上面的代码笔中,所示的昼/夜纹理基于相机相对于地球的位置,我需要那些相对于光源位置(而不是相机)保持固定。

    constructor(selector) {
        this.selector = selector;
        this.width = window.innerWidth;
        this.height = window.innerHeight;
        this.frameEvent = new Event('frame');

        this.textureLoader = new THREE.TextureLoader();
    }

    setScene() {
        this.scene = new THREE.Scene();
        this.scenary = new THREE.Object3D;

        this.scene.add(this.scenary);
    }

    setCamera() {
        this.camera = new THREE.PerspectiveCamera(50, this.width/this.height, 1, 20000);
        this.camera.position.y = 25;
        this.camera.position.z = 300;
    }

    setRenderer() {
        this.renderer = new THREE.WebGLRenderer({
            antialias: true
        });
        this.renderer.setSize(this.width, this.height);
        this.canvas = document.querySelector(this.selector).appendChild(this.renderer.domElement);
    }

    setControls() {
        this.controls = new THREE.OrbitControls(this.camera, this.canvas);
        this.controls.maxDistance = 500;
        this.controls.minDistance = 200;
    }

    addHelpers() {
        this.axes = new THREE.AxesHelper(500);
        this.scenary.add(this.axes);
    }

    addLights() {
        this.ambientLight = new THREE.AmbientLight(0x555555);
        this.directionalLight = new THREE.DirectionalLight(0xffffff);
        this.directionalLight.position.set(10, 0, 10).normalize();

        this.scenary.add(this.ambientLight);
        this.scenary.add(this.directionalLight);
    }

    render() {
        this.renderer.render(this.scene, this.camera);
        this.canvas.dispatchEvent(this.frameEvent);
        this.frameRequest = window.requestAnimationFrame(this.render.bind(this));
    }

    destroy() {
        window.cancelAnimationFrame(this.frameRequest);
        this.scene.children = [];
        this.canvas.remove();
    }

    addSky() {
        let radius = 400,
            segments = 50;

        this.skyGeometry = new THREE.SphereGeometry(radius, segments, segments);
        this.skyMaterial = new THREE.MeshPhongMaterial({
            color: 0x666666,
            side: THREE.BackSide,
            shininess: 0
        });
        this.sky = new THREE.Mesh(this.skyGeometry, this.skyMaterial);

        this.scenary.add(this.sky);

        this.loadSkyTextures();
    }

    loadSkyTextures() {
        this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/sky-texture.jpg', texture => {
            this.skyMaterial.map = texture;
            this.skyMaterial.needsUpdate = true;
        });
    }

    addEarth() {
        let radius = 100,
            segments = 50;

        this.earthGeometry = new THREE.SphereGeometry(radius, segments, segments);
        this.earthMaterial = new THREE.ShaderMaterial({
            bumpScale: 5,
            specular: new THREE.Color(0x333333),
            shininess: 50,
            uniforms: {
                sunDirection: {
                    value: new THREE.Vector3(1, 1, .5)
                },
                dayTexture: {
                    value: this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/earth-texture.jpg')
                },
                nightTexture: {
                    value: this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/earth-night.jpg')
                }
            },
            vertexShader: this.dayNightShader.vertex,
            fragmentShader: this.dayNightShader.fragment
        });
        this.earth = new THREE.Mesh(this.earthGeometry, this.earthMaterial);

        this.scenary.add(this.earth);

        this.loadEarthTextures();
        this.addAtmosphere();
    }

    loadEarthTextures() {
        this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/earth-texture.jpg', texture => {
            this.earthMaterial.map = texture;
            this.earthMaterial.needsUpdate = true;
        });
        this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/earth-bump.jpg', texture => {
            this.earthMaterial.bumpMap = texture;
            this.earthMaterial.needsUpdate = true;
        });
        this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/earth-specular.jpg', texture => {
            this.earthMaterial.specularMap = texture;
            this.earthMaterial.needsUpdate = true;
        });
    }

    addAtmosphere() {
        this.innerAtmosphereGeometry = this.earthGeometry.clone();
        this.innerAtmosphereMaterial = THREEx.createAtmosphereMaterial();
        this.innerAtmosphereMaterial.uniforms.glowColor.value.set(0x88ffff);
        this.innerAtmosphereMaterial.uniforms.coeficient.value = 1;
        this.innerAtmosphereMaterial.uniforms.power.value = 5;
        this.innerAtmosphere = new THREE.Mesh(this.innerAtmosphereGeometry, this.innerAtmosphereMaterial);
        this.innerAtmosphere.scale.multiplyScalar(1.008);

        this.outerAtmosphereGeometry = this.earthGeometry.clone();
        this.outerAtmosphereMaterial = THREEx.createAtmosphereMaterial();
        this.outerAtmosphereMaterial.side = THREE.BackSide;
        this.outerAtmosphereMaterial.uniforms.glowColor.value.set(0x0088ff);
        this.outerAtmosphereMaterial.uniforms.coeficient.value = .68;
        this.outerAtmosphereMaterial.uniforms.power.value = 10;
        this.outerAtmosphere = new THREE.Mesh(this.outerAtmosphereGeometry, this.outerAtmosphereMaterial);
        this.outerAtmosphere.scale.multiplyScalar(1.06);

        this.earth.add(this.innerAtmosphere);
        this.earth.add(this.outerAtmosphere);
    }

    get dayNightShader() {
        return {
            vertex: `
                varying vec2 vUv;
                varying vec3 vNormal;

                void main() {
                    vUv = uv;
                    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
                    vNormal = normalMatrix * normal;
                    gl_Position = projectionMatrix * mvPosition;
                }
            `,
            fragment: `
                uniform sampler2D dayTexture;
                uniform sampler2D nightTexture;

                uniform vec3 sunDirection;

                varying vec2 vUv;
                varying vec3 vNormal;

                void main(void) {
                    vec3 dayColor = texture2D(dayTexture, vUv).rgb;
                    vec3 nightColor = texture2D(nightTexture, vUv).rgb;

                    float cosineAngleSunToNormal = dot(normalize(vNormal), sunDirection);

                    cosineAngleSunToNormal = clamp(cosineAngleSunToNormal * 5.0, -1.0, 1.0);

                    float mixAmount = cosineAngleSunToNormal * 0.5 + 0.5;

                    vec3 color = mix(nightColor, dayColor, mixAmount);

                    gl_FragColor = vec4(color, 1.0);
                }
            `
        }
    }

    animate() {
        this.canvas.addEventListener('frame', () => {
            this.scenary.rotation.x += 0.0001;
            this.scenary.rotation.y -= 0.0005;
        });
    }

    init() {
        this.setScene();
        this.setCamera();
        this.setRenderer();
        this.setControls();
        this.addLights();
        this.render();
        this.addSky();
        this.addEarth();
        this.animate();
    }
}

let canvas = new Canvas('#canvas');
canvas.init();

据我所知,看起来着色器正在由get dayNightShader()中的相机更新。看起来modelViewMatrix,projectionMatrix和normalMatrix都是基于相机的,基于我可以在three.js文档中找到的内容,并且我尝试将它们更改为固定的矢量位置,但这是我唯一要做的看到它所做的就是隐藏地球并显示大气纹理。有没有一种方法可以使用光源的位置而不是照相机来确定着色器显示的内容?

1 个答案:

答案 0 :(得分:1)

问题是线

  
float cosineAngleSunToNormal = dot(normalize(vNormal), sunDirection); 

在片段着色器中。
vNormal是视图空间中的方向,因为它是由顶点着色器中的normalMatrix变换的,而sunDirection是世界空间方向。

要解决此问题,您必须通过顶点着色器中的视图矩阵转换阳光方向,并将转换后的方向矢量传递给片段着色器。

vSunDir = mat3(viewMatrix) * sunDirection;

请注意,viewMatrix从世界空间转换为视图空间。使用viewMatrix而不是normalMatrix很重要,因为normalMatrix从模型空间转换为世界空间。

顶点着色器:

varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vSunDir;

uniform vec3 sunDirection;

void main() {
    vUv = uv;
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);

    vNormal = normalMatrix * normal;
    vSunDir = mat3(viewMatrix) * sunDirection;

    gl_Position = projectionMatrix * mvPosition;
}

片段着色器:

uniform sampler2D dayTexture;
uniform sampler2D nightTexture;

varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vSunDir;

void main(void) {
    vec3 dayColor = texture2D(dayTexture, vUv).rgb;
    vec3 nightColor = texture2D(nightTexture, vUv).rgb;

    float cosineAngleSunToNormal = dot(normalize(vNormal), normalize(vSunDir));

    cosineAngleSunToNormal = clamp(cosineAngleSunToNormal * 5.0, -1.0, 1.0);

    float mixAmount = cosineAngleSunToNormal * 0.5 + 0.5;

    vec3 color = mix(nightColor, dayColor, mixAmount);

    gl_FragColor = vec4(color, 1.0);
}

class Canvas {
	constructor(selector) {
		this.selector = selector;
		this.width = window.innerWidth;
		this.height = window.innerHeight;
		this.frameEvent = new Event('frame');

		this.textureLoader = new THREE.TextureLoader();
	}

	setScene() {
		this.scene = new THREE.Scene();
		this.scenary = new THREE.Object3D;

		this.scene.add(this.scenary);
	}

	setCamera() {
		this.camera = new THREE.PerspectiveCamera(50, this.width/this.height, 1, 20000);
		this.camera.position.y = 25;
		this.camera.position.z = 300;
	}

	setRenderer() {
		this.renderer = new THREE.WebGLRenderer({
			antialias: true
		});
    this.renderer.setSize(this.width, this.height);
    var container = document.getElementById(this.selector);
    this.canvas = container.appendChild(this.renderer.domElement);
		//this.canvas = document.querySelector(this.selector).appendChild(this.renderer.domElement);
	}

	setControls() {
		this.controls = new THREE.OrbitControls(this.camera, this.canvas);
		this.controls.maxDistance = 500;
		this.controls.minDistance = 200;
	}

	addHelpers() {
		this.axes = new THREE.AxesHelper(500);
		this.scenary.add(this.axes);
	}

	addLights() {
		this.ambientLight = new THREE.AmbientLight(0x555555);
		this.directionalLight = new THREE.DirectionalLight(0xffffff);
		this.directionalLight.position.set(10, 0, 10).normalize();

		this.scenary.add(this.ambientLight);
		this.scenary.add(this.directionalLight);
	}

	render() {
		this.renderer.render(this.scene, this.camera);
		this.canvas.dispatchEvent(this.frameEvent);
		this.frameRequest = window.requestAnimationFrame(this.render.bind(this));
	}

	destroy() {
		window.cancelAnimationFrame(this.frameRequest);
		this.scene.children = [];
		this.canvas.remove();
	}

	addSky() {
		let radius = 400,
			segments = 50;

		this.skyGeometry = new THREE.SphereGeometry(radius, segments, segments);
		this.skyMaterial = new THREE.MeshPhongMaterial({
			color: 0x666666,
			side: THREE.BackSide,
			shininess: 0
		});
		this.sky = new THREE.Mesh(this.skyGeometry, this.skyMaterial);

		this.scenary.add(this.sky);

		this.loadSkyTextures();
	}

	loadSkyTextures() {
		this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/sky-texture.jpg', texture => {
			this.skyMaterial.map = texture;
			this.skyMaterial.needsUpdate = true;
		});
	}

	addEarth() {
		let radius = 100,
			segments = 50;

		this.earthGeometry = new THREE.SphereGeometry(radius, segments, segments);
		this.earthMaterial = new THREE.ShaderMaterial({
			bumpScale: 5,
			specular: new THREE.Color(0x333333),
			shininess: 50,
			uniforms: {
				sunDirection: {
					value: new THREE.Vector3(1, 1, .5)
				},
				dayTexture: {
					value: this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/earth-texture.jpg')
				},
				nightTexture: {
					value: this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/earth-night.jpg')
				}
			},
			vertexShader: this.dayNightShader.vertex,
			fragmentShader: this.dayNightShader.fragment
		});
		this.earth = new THREE.Mesh(this.earthGeometry, this.earthMaterial);

		this.scenary.add(this.earth);

		this.loadEarthTextures();
		this.addAtmosphere();
	}

	loadEarthTextures() {
		this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/earth-texture.jpg', texture => {
			this.earthMaterial.map = texture;
			this.earthMaterial.needsUpdate = true;
		});
		this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/earth-bump.jpg', texture => {
			this.earthMaterial.bumpMap = texture;
			this.earthMaterial.needsUpdate = true;
		});
		this.textureLoader.load('https://acaua.gitlab.io/webgl-with-threejs/img/textures/earth/earth-specular.jpg', texture => {
			this.earthMaterial.specularMap = texture;
			this.earthMaterial.needsUpdate = true;
		});
	}

	addAtmosphere() {
    /*
		this.innerAtmosphereGeometry = this.earthGeometry.clone();
		this.innerAtmosphereMaterial = THREEx.createAtmosphereMaterial();
		this.innerAtmosphereMaterial.uniforms.glowColor.value.set(0x88ffff);
		this.innerAtmosphereMaterial.uniforms.coeficient.value = 1;
		this.innerAtmosphereMaterial.uniforms.power.value = 5;
		this.innerAtmosphere = new THREE.Mesh(this.innerAtmosphereGeometry, this.innerAtmosphereMaterial);
		this.innerAtmosphere.scale.multiplyScalar(1.008);

		this.outerAtmosphereGeometry = this.earthGeometry.clone();
		this.outerAtmosphereMaterial = THREEx.createAtmosphereMaterial();
		this.outerAtmosphereMaterial.side = THREE.BackSide;
		this.outerAtmosphereMaterial.uniforms.glowColor.value.set(0x0088ff);
		this.outerAtmosphereMaterial.uniforms.coeficient.value = .68;
		this.outerAtmosphereMaterial.uniforms.power.value = 10;
		this.outerAtmosphere = new THREE.Mesh(this.outerAtmosphereGeometry, this.outerAtmosphereMaterial);
		this.outerAtmosphere.scale.multiplyScalar(1.06);

		this.earth.add(this.innerAtmosphere);
		this.earth.add(this.outerAtmosphere);
    */
	}

	get dayNightShader() {
		return {
			vertex: `
				varying vec2 vUv;
        varying vec3 vNormal;
        varying vec3 vSunDir;

        uniform vec3 sunDirection;

				void main() {
					vUv = uv;
					vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
          vNormal = normalMatrix * normal;
          vSunDir = mat3(viewMatrix) * sunDirection;
					gl_Position = projectionMatrix * mvPosition;
				}
			`,
			fragment: `
				uniform sampler2D dayTexture;
				uniform sampler2D nightTexture;

				varying vec2 vUv;
        varying vec3 vNormal;
        varying vec3 vSunDir;

				void main(void) {
					vec3 dayColor = texture2D(dayTexture, vUv).rgb;
					vec3 nightColor = texture2D(nightTexture, vUv).rgb;

					float cosineAngleSunToNormal = dot(normalize(vNormal), normalize(vSunDir));

					cosineAngleSunToNormal = clamp(cosineAngleSunToNormal * 5.0, -1.0, 1.0);

					float mixAmount = cosineAngleSunToNormal * 0.5 + 0.5;

					vec3 color = mix(nightColor, dayColor, mixAmount);

					gl_FragColor = vec4(color, 1.0);
				}
			`
		}
	}

	animate() {
		this.canvas.addEventListener('frame', () => {
			this.scenary.rotation.x += 0.0001;
			this.scenary.rotation.y -= 0.0005;
		});
	}

	init() {
		this.setScene();
		this.setCamera();
		this.setRenderer();
		this.setControls();
		this.addLights();
		this.render();
		this.addSky();
		this.addEarth();
		this.animate();
	}
}

let canvas = new Canvas('container');
canvas.init();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/106/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<div id="container"></div>