为WebGL / Three.js地球添加夜间灯光

5
我正在使用Three.js作为开发太空模拟器的框架,但是我尝试使夜间灯光工作,却失败了。
这里可以访问模拟器:

orbitingeden.com

以下是代码片段,可以在此页面找到:

orbitingeden.com/orrery/soloearth.html

示例页面的代码在此处。我甚至不知道从哪里开始。我尝试渲染两个球体,相距几个单位,一个靠近太阳(白天版本),一个远离太阳(夜间版本),但存在许多问题,其中最重要的是它们会以奇怪的十二面体方式开始交叠。我采用了orrery中的tDiffuse2想法,但无法使其工作。

<!doctype html>
<html lang="en">
    <head>
        <title>three.js webgl - earth</title>
        <meta charset="utf-8">
        <script src="three.js/Detector.js"></script>
        <script src="three.js/Three.js"></script>
    </head>
    <body>
        <script>
            if ( ! Detector.webgl ) Detector.addGetWebGLMessage();

            var radius = 6371;
            var tilt = 0.41;
            var rotationSpeed = 0.02;
            var cloudsScale = 1.005;
            var SCREEN_HEIGHT = window.innerHeight;
            var SCREEN_WIDTH  = window.innerWidth;
            var container, camera, scene, renderer;
            var meshPlanet, meshClouds, dirLight, ambientLight;
            var clock = new THREE.Clock();

            init();
            animate();

            function init() {
                container = document.createElement( 'div' );
                document.body.appendChild( container );

                scene = new THREE.Scene();
                scene.fog = new THREE.FogExp2( 0x000000, 0.00000025 );

                camera = new THREE.PerspectiveCamera( 25, SCREEN_WIDTH / SCREEN_HEIGHT, 50, 1e7 );
                camera.position.z = radius * 5;
                scene.add( camera );

                dirLight = new THREE.DirectionalLight( 0xffffff );
                dirLight.position.set( -20, 0, 2 ).normalize();
                scene.add( dirLight );

                ambientLight = new THREE.AmbientLight( 0x000000 );
                scene.add( ambientLight );

                //initialize the earth
                var planetTexture = THREE.ImageUtils.loadTexture( "textures/earth-day.jpg" ),
                nightTexture      = THREE.ImageUtils.loadTexture( "textures/earthNight.gif" ),
                cloudsTexture     = THREE.ImageUtils.loadTexture( "textures/clouds.gif" ),
                normalTexture     = THREE.ImageUtils.loadTexture( "textures/earth-map.jpg" ),
                specularTexture   = THREE.ImageUtils.loadTexture( "textures/earth-specular.jpg" );
                var shader = THREE.ShaderUtils.lib[ "normal" ];
                var uniforms = THREE.UniformsUtils.clone( shader.uniforms );
                uniforms[ "tNormal" ].texture = normalTexture;
                uniforms[ "uNormalScale" ].value = 0.85;
                uniforms[ "tDiffuse" ].texture = planetTexture;
                uniforms[ "tDiffuse2" ].texture = nightTexture;
                uniforms[ "tSpecular" ].texture = specularTexture;
                uniforms[ "enableAO" ].value = false;
                uniforms[ "enableDiffuse" ].value = true;
                uniforms[ "enableSpecular" ].value = true;
                uniforms[ "uDiffuseColor" ].value.setHex( 0xffffff );
                uniforms[ "uSpecularColor" ].value.setHex( 0x333333 );
                uniforms[ "uAmbientColor" ].value.setHex( 0x000000 );
                uniforms[ "uShininess" ].value = 15;
                var parameters = {
                    fragmentShader: shader.fragmentShader,
                    vertexShader: shader.vertexShader,
                    uniforms: uniforms,
                    lights: true,
                    fog: true
                };
                var materialNormalMap = new THREE.ShaderMaterial( parameters );
                geometry = new THREE.SphereGeometry( radius, 100, 50 );
                geometry.computeTangents();
                meshPlanet = new THREE.Mesh( geometry, materialNormalMap );
                meshPlanet.rotation.y = 0;
                meshPlanet.rotation.z = tilt;
                scene.add( meshPlanet );

                // clouds
                var materialClouds = new THREE.MeshLambertMaterial( { color: 0xffffff, map: cloudsTexture, transparent: true } );
                meshClouds = new THREE.Mesh( geometry, materialClouds );
                meshClouds.scale.set( cloudsScale, cloudsScale, cloudsScale );
                meshClouds.rotation.z = tilt;
                scene.add( meshClouds );

                renderer = new THREE.WebGLRenderer( { clearColor: 0x000000, clearAlpha: 1 } );
                renderer.setSize( SCREEN_WIDTH, SCREEN_HEIGHT );
                renderer.sortObjects = false;
                renderer.autoClear = false;
                container.appendChild( renderer.domElement );
            };

            function animate() {
                requestAnimationFrame( animate );
                render();
            };

            function render() {
                // rotate the planet and clouds
                var delta = clock.getDelta();
                meshPlanet.rotation.y += rotationSpeed * delta;
                meshClouds.rotation.y += 1.25 * rotationSpeed * delta;
                //render the scene
                renderer.clear();
                renderer.render( scene, camera );
            };
        </script>
    </body>
</html>
2个回答

18

如果我理解您的问题......

我不了解 three.js,但通常我会通过使用着色器来传递白天和夜晚的纹理,并在着色器中选择其中一个。例如:

uniform sampler2D dayTexture;
uniform sampler2D nightTexture;
varying vec3 v_surfaceToLight;  // assumes this gets passed in from vertex shader
varying vec4 v_normal;          // assumes this gets passed in from vertex shader
varying vec2 v_texCoord;        // assumes this gets passed in from vertex shader

void main () {
   vec3 normal = normalize(v_normal);
   vec3 surfaceToLight = normalize(v_surfaceToLight);
   float angle = dot(normal, surfaceToLight);
   vec4 dayColor = texture2D(dayTexture, v_texCoords);
   vec4 nightColor = texture2D(nightTexture, v_texCoord);
   vec4 color = angle < 0.0 ? dayColor : nightColor;

   ...

   gl_FragColor = color * ...;
}

基本上,您采用照明计算,而不是将其用于照明,而是用它来选择纹理。照明计算通常使用表面法线和从表面到光源(太阳)的方向之间的点积。这给出了两个向量之间角度的余弦值。余弦值范围从-1到1,因此如果该值介于-1到0之间,则面朝远离太阳的方向,如果为0到+1,则面朝向太阳的方向。

该行

   vec4 color = angle < 0.0 ? dayColor : nightColor;

选择白天或黑夜。这将是一个严格的截断点。您可以尝试使用更模糊的东西

   // convert from -1 <-> +1 to 0 <-> +1
   float lerp0To1 = angle * 0.5 + 0.5; 

   // mix between night and day
   vec4 color = mix(nightColor, dayColor, lerp0to1);

这将使你在直接面对太阳的地方获得100%的白天和在直接相反太阳的地方获得100%的黑夜,中间是混合的。可能不是你想要的,但你可以尝试调整数字。例如:

   // sharpen the mix
   angle = clamp(angle * 10.0, -1.0, 1.0);

   // convert from -1 <-> +1 to 0 <-> +1
   float lerp0To1 = angle * 0.5 + 0.5; 

   // mix between night and day
   vec4 color = mix(nightColor, dayColor, lerp0to1);

希望这有意义。


所以我花了一点时间来编写一个 Three.js 示例,部分是为了学习 Three.js。示例在此处。

const vs = `
varying vec2 vUv;
varying vec3 vNormal;

void main() {
  vUv = uv;
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
  vNormal = normalMatrix * normal;
  gl_Position = projectionMatrix * mvPosition;
}
`;

const fs = `
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;

  // compute cosine sun to normal so -1 is away from sun and +1 is toward sun.
  float cosineAngleSunToNormal = dot(normalize(vNormal), sunDirection);

  // sharpen the edge beween the transition
  cosineAngleSunToNormal = clamp( cosineAngleSunToNormal * 10.0, -1.0, 1.0);

  // convert to 0 to 1 for mixing
  float mixAmount = cosineAngleSunToNormal * 0.5 + 0.5;

  // Select day or night texture based on mix.
  vec3 color = mix( nightColor, dayColor, mixAmount );

  gl_FragColor = vec4( color, 1.0 );
}

`;

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(40, 1, 1, 3000);
camera.position.z = 4;
scene.add( camera );

const directionalLight = new THREE.DirectionalLight( 0xaaff33, 0 );
directionalLight.position.set(-1, 1, 0.5).normalize();
scene.add( directionalLight );

const textureLoader = new THREE.TextureLoader();

const uniforms = {
  sunDirection: {value: new THREE.Vector3(0,1,0) },
  dayTexture: { value: textureLoader.load( "https://i.imgur.com/dfLCd19.jpg" ) },
  nightTexture: { value: textureLoader.load( "https://i.imgur.com/MeKgLts.jpg" ) }
};

const material = new THREE.ShaderMaterial({
  uniforms: uniforms,
  vertexShader: vs,
  fragmentShader: fs,
});

const mesh = new THREE.Mesh( new THREE.SphereGeometry( 0.75, 32, 16 ), material );
scene.add( mesh );

renderer = new THREE.WebGLRenderer();
document.body.appendChild(renderer.domElement);
resize(true);
requestAnimationFrame(render);

function resize(force) {
  const canvas = renderer.domElement;
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  if (force || canvas.width !== width || canvas.height !== height) {
    renderer.setSize(width, height, false);
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
  }
}

function render(time) {
  time *= 0.001;  // seconds
  
  resize();
  
  uniforms.sunDirection.value.x = Math.sin(time);
  uniforms.sunDirection.value.y = Math.cos(time);

  // Note: Since the earth is at 0,0,0 you can set the normal for the sun
  // with
  //
  // uniforms.sunDirection.value.copy(sunPosition);
  // uniforms.sunDirection.value.normalize();


  mesh.rotation.y = time * .3
  mesh.rotation.x = time * .7;

  renderer.render(scene, camera);

  requestAnimationFrame(render);
}
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.min.js"></script>

我使用的着色器是这个

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;

    // compute cosine sun to normal so -1 is away from sun and +1 is toward sun.
    float cosineAngleSunToNormal = dot(normalize(vNormal), sunDirection);

    // sharpen the edge beween the transition
    cosineAngleSunToNormal = clamp( cosineAngleSunToNormal * 10.0, -1.0, 1.0);

    // convert to 0 to 1 for mixing
    float mixAmount = cosineAngleSunToNormal * 0.5 + 0.5;

    // Select day or night texture based on mixAmount.
    vec3 color = mix( nightColor, dayColor, mixAmount );

    gl_FragColor = vec4( color, 1.0 );

    // comment in the next line to see the mixAmount
    //gl_FragColor = vec4( mixAmount, mixAmount, mixAmount, 1.0 );
}

与前面的不同之处在于,由于太阳距离地球非常遥远,因此通常被认为是定向光源,因此您只需要知道其方向。换句话说,它相对于地球指向哪个方向。


你的 WebGL 教程非常棒,我会需要学习 WebGL,所以很高兴有人能够这么好地解释我的问题。如果没有其他人提供直接的 THREE.js 解决方案,我将尝试将你的解决方案整合进来,如果它有效,我会给你打上绿色勾勾。你知道如何将这个解决方案整合到我的当前 THREE.js 工作中吗?还是说我需要直接使用 WebGL 定义整个地球? - Orbiting Eden
您因发布示例而获得了应得的绿色勾选标志。现在轮到我来实现了。非常感谢! - Orbiting Eden
嗨,我正在尝试通过修改THREE.js现有的法线贴图着色器来实现这个功能。出于某种原因,我只能看到所有值为1.0以外的夜间纹理,而这些值应该显示白天纹理。换句话说,我似乎无法获得平滑混合。有什么想法吗? - korona
1
这个示例适用于你使用的Three.js版本(版本49),但不适用于更近期的版本(版本62或69)。地球从每个方向都是完全黑色的。为了让它与当前版本的Three.js一起工作,我需要在你的代码中做出哪些改变? - atmelino
@Kaëris 不过如果我这么做了,我会将其发布为答案。 - atmelino
显示剩余2条评论

0

感谢分享,非常有用。虽然我现在不确定为什么当相机旋转时阴影不会背对着太阳(它相对于相机保持静止)。这是我正在使用的设置sunDirection uniform的代码:

this.uniforms.sunDirection.value.copy(this.sunPosition); this.uniforms.sunDirection.value.normalize();

不确定为什么...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接