Boxy and terrible shadows


Increasing the shadow map width and height obviously solves the problem, but I would need to increase it to a massive number (which will obviously kill the performance)

I noticed that this issue arises when the body is very far from the spotlight. If I put the planet in a position which is closer to the spotlight, the shadows look much less boxy.

I would highly appreciate any help.

Code:

import * as THREE from "three";
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

// SUN
import sunMap from "../assets/textures/sun/sunmap.jpg";


// SATURN
import { getFresnelMatSAT } from "./saturnGlowMat.js";
import saturnmap from "../assets/textures/saturn/th_saturn.png";
import saturnbump from "../assets/textures/saturn/th_saturnbump.png";
import saturnclouds from "../assets/textures/saturn/th_saturnclouds.png";
import saturnlights from "../assets/textures/saturn/th_saturnnight.png";
import saturnrings from "../assets/textures/saturn/t00fri_gh_saturnrings.png";


const w = window.innerWidth;
const h = window.innerHeight;
const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(
    75, // fov
    w / h, // aspect ratio
    0.1, // near
    2000 // far
);
camera.position.y = 10;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap


renderer.setClearColor(0x000000)
renderer.setSize(w, h);
document.body.appendChild(renderer.domElement);
renderer.setPixelRatio( window.devicePixelRatio );
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.outputColorSpace = THREE.LinearSRGBColorSpace;

const controls = new OrbitControls(camera, renderer.domElement);
const detail = 12;
const loader = new THREE.TextureLoader();


// Create the cube geometry
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);

// Create the cube material
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); // Green color

// Create the cube mesh
const cubeMesh = new THREE.Mesh(cubeGeometry, cubeMaterial);

// Add the cube to the scene
// scene.add(cubeMesh);


const smallcubeGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const smallcubeMaterial = new THREE.MeshBasicMaterial({color: 0xFF00FF});
const smallcubeMesh = new THREE.Mesh(smallcubeGeometry, smallcubeMaterial);
// scene.add(smallcubeMesh);

cubeMesh.position.set(0, 0, 0)

// MERCURY ORBIT
const semiMajorAxis = 0.387; // scaled value, use a realistic scale for your scene
const eccentricity = 0.2056;
const semiMinorAxis = semiMajorAxis * Math.sqrt(1 - Math.pow(eccentricity, 2));

// Create the orbit path
const orbitCurve = new THREE.EllipseCurve(
  0, 0,             // Center of the ellipse (the Sun)
  semiMajorAxis,    // xRadius (semi-major axis)
  semiMinorAxis     // yRadius (semi-minor axis)
);

// Generate points from the curve
const points = orbitCurve.getPoints(100); // 100 points for a smooth orbit

// Create a geometry from the points
const orbitGeometry = new THREE.BufferGeometry().setFromPoints(points);

const orbitMaterial = new THREE.LineBasicMaterial({ color: 0xff0000 });
const orbit = new THREE.Line(orbitGeometry, orbitMaterial);
scene.add(orbit);

// Tilt the orbit to match Mercury's inclination
orbit.rotation.x = (90+7.004) * Math.PI / 180;




function createOrbit(semiMajorAxis, semiMinorAxis, inclination, orbitColor){
	const orbitCurve = new THREE.EllipseCurve(
		0, 0,
		semiMajorAxis, 
		semiMinorAxis
	  );
	const points = orbitCurve.getPoints(100) // CONST
	const orbitGeometry = new THREE.BufferGeometry().setFromPoints(points);

	const orbitMaterial = new THREE.LineBasicMaterial({ 
  		color: orbitColor,
  		transparent: true,
  		opacity: 0.3, // CONST
});
	const orbit = new THREE.Line(orbitGeometry, orbitMaterial);
	scene.add(orbit);
	orbit.rotation.x = THREE.MathUtils.degToRad(90) + inclination;
}



// SUN
///////////////////////////// SUN ///////////////////////////

const sunGroup = new THREE.Group();
scene.add(sunGroup);

sunGroup.rotation.y = 7.25 * Math.PI / 180;
sunGroup.rotation.x = -7.25 * Math.PI / 180;

const sunGeometry = new THREE.IcosahedronGeometry(1, detail);
const sunMaterial = new THREE.MeshBasicMaterial({
  map: loader.load(sunMap)
})

const sunMesh = new THREE.Mesh(sunGeometry, sunMaterial);
sunGroup.add(sunMesh);

/*
const sunfluidMaterial = new THREE.MeshBasicMaterial({
  map: loader.load(sunFluid),
  transparent: true,
  opacity: 1,
})

const sunfluidMesh = new THREE.Mesh(sunGeometry, sunfluidMaterial);
sunGroup.add(sunfluidMesh);
sunfluidMesh.scale.setScalar(1.015);
*/

const sunlight = new THREE.PointLight( 0xffffff, 2.5, 500, 0, 0, 0);
sunGroup.add(sunlight);
sunlight.castShadow = true;

sunlight.shadow.mapSize.width = 1024;  // improve shadow quality
sunlight.shadow.mapSize.height = 1024; // default is 512

sunGroup.position.set(0, 0, 0);

//
////////////////////////// SATURN //////////////////////////
const saturnGroup = new THREE.Group();
saturnGroup.rotation.y = 26.73 * Math.PI / 180;
saturnGroup.rotation.x = -26.73 * Math.PI / 180;
scene.add(saturnGroup);

const saturnGeometry = new THREE.IcosahedronGeometry(1, detail);
const saturnMaterial = new THREE.MeshPhongMaterial({
  map: loader.load(saturnmap),
  bumpMap: loader.load(saturnbump),
  bumpScale: 3,
});

const saturnMesh = new THREE.Mesh(saturnGeometry, saturnMaterial);
saturnGroup.add(saturnMesh);


const saturnNightlightsMat = new THREE.MeshBasicMaterial({
  map: loader.load(saturnlights),
  blending: THREE.AdditiveBlending,
});
const saturnNightlightsMesh = new THREE.Mesh(saturnGeometry, saturnNightlightsMat);
saturnGroup.add(saturnNightlightsMesh);

const saturnCloudsMat = new THREE.MeshStandardMaterial({
  map: loader.load(saturnclouds),
  transparent: true,
  opacity: 0.4,
  blending: THREE.AdditiveBlending,
});
const saturnCloudsMesh = new THREE.Mesh(saturnGeometry, saturnCloudsMat);
saturnCloudsMesh.scale.setScalar(1.01);
saturnGroup.add(saturnCloudsMesh);

const fresnelMatSAT = getFresnelMatSAT();
const saturnGlowMesh = new THREE.Mesh(saturnGeometry, fresnelMatSAT);
saturnGlowMesh.scale.setScalar(1.011);
saturnGroup.add(saturnGlowMesh);


// RING

const saturnRingGeometry = new THREE.RingGeometry(1, 2.5, 64);
const saturnRingMaterial = new THREE.MeshStandardMaterial({
  map: loader.load(saturnrings),
  color: 0xffffff,  
  side: THREE.DoubleSide,
  transparent: true,  
  opacity: 1,
});

const saturnRingMesh = new THREE.Mesh(saturnRingGeometry, saturnRingMaterial);

saturnRingMesh.rotation.x = Math.PI / 2;

saturnGroup.add(saturnRingMesh);
// saturnGroup.scale.set(0.084, 0.084, 0.084);


saturnMesh.castShadow = true; //default is false
saturnMesh.receiveShadow = false; //default

saturnRingMesh.receiveShadow = true;

//
let startangle = 1.15;
function updatePlanetPosition(semiMajorAxis, eccentricity, inclination, group, angle){
    const DISTANCE_SCALE = 10 // CONST
    semiMajorAxis *= 10;
    inclination = THREE.MathUtils.degToRad(inclination);
    const semiMinorAxis = semiMajorAxis * Math.sqrt(1 - Math.pow(eccentricity, 2));
  
    const x = semiMajorAxis * Math.cos(angle);
    const z = semiMinorAxis * Math.sin(angle);
    const y = z * -Math.sin(inclination);
    const correctedZ = z * Math.cos(inclination);
  
    group.position.set(x, y, correctedZ);
  }


  updatePlanetPosition(9.573, 0.0520, 2.486, saturnGroup, startangle); // saturn
  // startangle += 0.025
controls.target.copy(saturnGroup.position);
controls.update();
camera.position.set(saturnGroup.position.x ,saturnGroup.position.y + 5, saturnGroup.position.z);
camera.lookAt(saturnGroup.position);

sunlight.shadow.radius=5;
sunlight.shadow.blurSamples=10;

function render() {

requestAnimationFrame(render);



//
    updatePlanetPosition(9.573, 0.0520, 2.486, saturnGroup, startangle); // saturn
    // startangle += 0.025

	renderer.render(scene, camera);
}
  render();

  function handleWindowResize () {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
  }


Based on how big your scene is you can adjust the lights near and far values.
https://threejs.org/docs/index.html?q=LIGHT#api/en/lights/PointLight.shadow
The values should be as compact as possible to achieve the best results.
Hope that helps!

1 Like


This was as compact as I could get, but unfortunately:


Shadow still looks like it came out from Minecraft.

Can you add a camera helper to visualize the entire bounds of your shadow cameras bounding box and post a pic?

const shadowHelper = new THREE.CameraHelper(light.shadow.camera);   
scene.add(shadowHelper)
1 Like


Here! It seems that Saturn isn’t even inside the range, but I’m not sure how to fix that.

:eyes::face_with_monocle: Are the left right top and bottom of that frustum intentionally that big for a reason? If you set the light to look at saturn and then find the minimal - maximal bounds of saturns group, you can apply those bounds to the shadow cameras left right top bottom and even near far planes… (++ a little padding for safe measure)

No, I think that’s just the default. Maybe it’s because of the distance of the spotlight itself? I made it to cover an entire solar system.

Also, I’m a little confused but I’ll try the approach you suggested. Thanks a lot for your help!

yeah try have a play with the shadow cams bounding frustum while you’ve got the visual there, it’ll help a lot, what I mean by use saturns group to determine the values…

const saturnBounds = new Box3().setFromObject(saturnGroup)

sunlight.shadow.camera.left = saturnBounds.min.x
sunlight.shadow.camera.right = saturnBounds.max.x
// etc etc for top bottom... min max Y

for near and far planes you can use the formula you used originally which was correct but you can modify this to first apply the saturnBounds min max z and then add your light distance to both…

1 Like

Come to review the question I’ve overlooked what @kalabedo picked up on initially, the source of light is a point light, of which the shadow camera is a perspective camera, not orthagonal like a directional light, meaning the shadow camera fov, near and far need to be calculated, not top, right, bottom and left :expressionless::thinking: @xlpy are you using PointLight over DirectionalLight for an integral purpose?

1 Like

You may have to adjust the shadow.bias. I didn’t used to have to do that with WebGL, but perhaps things have changed.