How to cast shadows from an outer sphere to an inner sphere

Hi everyone, I’m trying to create a realistic Earth in three.js. Currently I have two SphereGeometries, the Earth and the Clouds. I want to find a way for the Clouds to cast shadows onto the inner Earth sphere, but I don’t have a clue yet.

The result currently looks something like this:

Code for the Earth:

let earthGeo = new THREE.SphereGeometry(10, 64, 64)
let earthMat = new THREE.MeshStandardMaterial({
  map: albedoMap,
  bumpMap: bumpMap,
  bumpScale: 0.03, // must be really small, if too high even bumps on the back side got lit up
})
this.earth = new THREE.Mesh(earthGeo, earthMat)
scene.add(this.earth)

Code for the Clouds:

let cloudGeo = new THREE.SphereGeometry(10.05, 64, 64)
let cloudsMat = new THREE.MeshPhongMaterial({
  map: cloudsMap,
  alphaMap: cloudsMap,
  transparent: true
})
this.clouds = new THREE.Mesh(cloudGeo, cloudsMat)
scene.add(this.clouds)

Code for my DirectionalLight:

this.dirLight = new THREE.DirectionalLight()
this.dirLight.position.set(-50, 50, 0)
this.dirLight.castShadow = true
scene.add(this.dirLight)

I’m using version 0.151.3 for Three.js.
If anyone can point me in the right direction I will be very much grateful!!!

according to this it should work if you specify alphaTest or custom depth material on the clouds

2 Likes

Thanks for the prompt reply! I’ve just tried to add the alphaTest attribute onto my Clouds material and make sure shadow related properties are set, but still it doesn’t seem to work for me.

Here’s my full code in index.js:

// ThreeJS and Third-party deps
import * as THREE from "three"
import * as dat from 'dat.gui'
import Stats from "three/examples/jsm/libs/stats.module"
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"

// Core boilerplate code deps
import { createCamera, createComposer, createRenderer, runApp } from "./core-utils"

// Other deps
import Albedo from "./assets/Albedo.jpg"
import Clouds from "./assets/Clouds.png"
import Bump from "./assets/Bump.jpg"

global.THREE = THREE
THREE.ColorManagement.enabled = true

let scene = new THREE.Scene()

let renderer = createRenderer({ antialias: true }, (_renderer) => {
  // best practice: ensure output colorspace is in sRGB, see Color Management documentation:
  // https://threejs.org/docs/#manual/en/introduction/Color-management
  _renderer.outputEncoding = THREE.sRGBEncoding
  _renderer.shadowMap.enabled = true
})

let camera = createCamera(45, 1, 1000, { x: 0, y: 0, z: 30 })

let app = {
  async initScene() {
    // OrbitControls
    this.controls = new OrbitControls(camera, renderer.domElement)
    this.controls.enableDamping = true

    this.dirLight = new THREE.DirectionalLight()
    this.dirLight.position.set(-50, 50, 0)
    this.dirLight.castShadow = true
    scene.add(this.dirLight)

    const albedoMap = await this.loadTexture(Albedo)
    const cloudsMap = await this.loadTexture(Clouds)
    const bumpMap = await this.loadTexture(Bump)
    
    let earthGeo = new THREE.SphereGeometry(10, 64, 64)
    let earthMat = new THREE.MeshStandardMaterial({
      map: albedoMap,
      bumpMap: bumpMap,
      bumpScale: 0.03, // must be really small, if too high even bumps on the back side got lit up
    })
    this.earth = new THREE.Mesh(earthGeo, earthMat)
    this.earth.receiveShadow = true
    scene.add(this.earth)

    let cloudGeo = new THREE.SphereGeometry(10.05, 64, 64)
    let cloudsMat = new THREE.MeshStandardMaterial({
      map: cloudsMap,
      alphaMap: cloudsMap,
      transparent: true,
      alphaTest: 0.1
    })
    this.clouds = new THREE.Mesh(cloudGeo, cloudsMat)
    this.clouds.castShadow = true
    scene.add(this.clouds)

    // GUI controls
    const gui = new dat.GUI()

    // Stats - show fps
    this.stats1 = new Stats()
    this.stats1.showPanel(0) // Panel 0 = fps
    this.stats1.domElement.style.cssText = "position:absolute;top:0px;left:0px;"
    // this.container is the parent DOM element of the threejs canvas element
    this.container.appendChild(this.stats1.domElement)
  },
  async loadTexture(url) {
    this.textureLoader = this.textureLoader || new THREE.TextureLoader()
    return new Promise(resolve => {
      this.textureLoader.load(url, texture => {
        resolve(texture)
      })
    })
  },
  // @param {number} interval - time elapsed between 2 frames
  // @param {number} elapsed - total time elapsed since app start
  updateScene(interval, elapsed) {
    this.controls.update()
    this.stats1.update()

    this.earth.rotation.y += interval * 0.006
    this.clouds.rotation.y += interval * 0.01
  }
}

runApp(app, scene, renderer, camera, true, undefined, undefined)

welp, your clouds are facing outwards, so you probably need to make them double-sided

1 Like

Wow it works! You saved my day! Lots of love and respect :raised_hands:

You can actually simply use the clouds texture as direct shadow multiplying by inversed alpha. It requires a little shader change but the result will look much smoother, more stable and is cheaper than shadow mapping. You can also displace/deform the texture by light direction.

5 Likes

Sounds more complicated if that includes tweaking shaders but I’d like to learn more actually. Could you elaborate a little more so that I can further explore this option :raised_hands:! I do know a little bit about shaders but I’m not familiar with handling lighting in shaders yet.

once in a while fyrestar has good ideas, and this is one of those times. to implement it, you will need to 1) learn glsl, 2) look up material.onBeforeCompile example (fyrestar has some helper for that, Im sure it will be linked to in a reply), and 3) study 3js shader chunks to find the place to make your changes. this does sound complicated, but once you are at least on step 2, it is not. and the result should be great, too.

1 Like

Is that you Dawg? :face_with_raised_eyebrow:

You should be familiar with shaders, here is some pseudocode how a simple approach would look like, assuming earth and cloud are a sphere with same UV coordinates.

earthMaterial.onBeforeCompile = function( shader ) {

	shader.uniforms.tClouds = { value: cloudTexture };
	shader.fragmentShader = shader.fragmentShader.replace('#include <map_pars_fragment>', '#include <map_pars_fragment>\nuniform sampler2D tClouds;');
	shader.fragmentShader = shader.fragmentShader.replace('#include <emissivemap_fragment>', `#include <emissivemap_fragment>
	diffuseColor.rgb *= max( 1.0 - texture( tClouds, vUv ).a, 0.2 ); // Clamp it up so it doesn't get too dark unless you want
	`);

}

You can also smoothstep or pow the alpha of the clouds to get thicker shadows/different falloff, displacing/deforming by light direction is another addition you could add.

2 Likes

Thanks a ton @makc3d and @Fyrestar! I will definitely try understand and implement this. Exciting to learn something new here!

What type of shadow will it generate? The left one (shadows fall down as rain) or the right one (shaodws made from distant sun)?

1 Like

The pseudocode will be the left, for the right you need to do the directional displacement also dotting light direction with the surface normal then so at the horizon they fade out.

2 Likes

Although this topic is already solved, here is a way to achieve a similar effect without shadow casting and without custom shaders. The texture for the clouds is reused as a negative light map for the earth.

https://codepen.io/boytchev/full/GRwyNjq

image

6 Likes

Thanks Pavel! never thought about doing it this way :+1:

1 Like

That was just what i suggested? :smiling_face_with_tear: Wether using as lightmap or with the 2 line code addition for a dedicated feature, for it to be actual shadows you need a code addition with displaced UV according to the direction/surface normal.

1 Like

I got your idea implemented via onBeforeCompile, it works and looking good :+1:

2 Likes