Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
526c2d3
es6 convert ray class
lukemckinstry Apr 7, 2026
7af31ab
Enhance shadow casting: include hasDiscard in shader keywords and upd…
Masty88 Apr 8, 2026
31e8fd3
Refactor shadow map shader tests to directly check new main source in…
Masty88 Apr 8, 2026
6ded377
Add demo for shadow casting with clipping polygons
Masty88 Apr 8, 2026
5734b79
Fix clipping planes matrix computation for shadow casting
Masty88 Apr 8, 2026
5363740
Remove unused clippingPlanesMatrix initialization in Model constructor
Masty88 Apr 9, 2026
c1fadcb
Merge branch 'main' into fix/double-cast
Masty88 Apr 9, 2026
cc7fba1
Enhance default clipping planes test by adding uniform state to mock …
Masty88 Apr 9, 2026
1eeca62
Refactor model matrix handling in clipping planes processing to use d…
Masty88 Apr 10, 2026
29128c7
Add JSDoc comments for shadow casting shader functions
Masty88 Apr 10, 2026
7ab6fde
Merge branch 'main' into fix/double-cast
Masty88 Apr 10, 2026
c0c65c1
Merge branch 'main' into fix/double-cast
Masty88 Apr 21, 2026
56f838c
ModelClippingPlanesPipelineStage: Use scratchClippingPlanesMatrix2 fo…
Masty88 Apr 21, 2026
fcf8321
Remove unused _clippingPlanesMatrix from mockModel in ModelClippingPl…
Masty88 Apr 21, 2026
3d70a90
Implement discard detection in ShadowMapShader and add corresponding …
Masty88 Apr 21, 2026
001eb9a
Fix model_clippingPlanesMatrix reference in clipping planes test
Masty88 Apr 21, 2026
cdd3a09
Merge branch 'main' into fix/double-cast
Masty88 Apr 22, 2026
e78a7f3
Merge branch 'main' into fix/double-cast
Masty88 Apr 23, 2026
2c96918
Merge branch 'main' into fix/double-cast
Masty88 Apr 25, 2026
4bf206b
Merge branch 'main' into fix/double-cast
Masty88 Apr 27, 2026
3323044
Merge branch 'main' into fix/double-cast
Masty88 May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions Apps/Sandcastle/gallery/Shadows Clipping Polygon.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<meta
name="description"
content="Demonstrates that clipping polygons correctly suppress shadow casting in clipped regions."
/>
<meta name="cesium-sandcastle-labels" content="Showcases" />
<title>Cesium Demo</title>
<script type="text/javascript" src="../Sandcastle-header.js"></script>
<script
type="text/javascript"
src="../../../Build/CesiumUnminified/Cesium.js"
nomodule
></script>
<script type="module" src="../load-cesium-es6.js"></script>
</head>
<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
<style>
@import url(../templates/bucket.css);
#toolbar .cesium-button {
display: block;
}
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar"></div>

<script id="cesium_sandcastle_script">
window.startup = async function (Cesium) {
"use strict";

Check failure on line 37 in Apps/Sandcastle/gallery/Shadows Clipping Polygon.html

View workflow job for this annotation

GitHub Actions / lint

'use strict' is unnecessary inside of modules
//Sandcastle_Begin

// Demonstrates fix for: Shadows ignore clipping planes #6261
// Two cases shown:
// 1. OSM buildings clipped via ClippingPolygonCollection — cleared area
// with a WoodTower; removed buildings must not cast ghost shadows.
// 2. CesiumAir clipped via ClippingPlaneCollection — rear half removed;
// the missing half must not cast a shadow (mirrors original bug report).
// Toggle "Clipping" off to see ghost shadows reappear for both cases.

const viewer = new Cesium.Viewer("cesiumContainer", {
infoBox: false,
selectionIndicator: false,
shadows: true,
terrainShadows: Cesium.ShadowMode.ENABLED,
shouldAnimate: false,
terrain: Cesium.Terrain.fromWorldTerrain(),
});

const shadowMap = viewer.shadowMap;
shadowMap.maximumDistance = 3000.0;
shadowMap.size = 2048;

// Neuchâtel, Switzerland — 09:00 UTC (11:00 CEST), sun ESE
viewer.clock.currentTime = Cesium.JulianDate.fromIso8601("2024-06-21T09:00:00Z");

const CENTER_LON = 6.9288;
const CENTER_LAT = 46.9919;

// ── OSM Buildings ────────────────────────────────────────────────────
const osmBuildings = await Cesium.createOsmBuildingsAsync();
osmBuildings.shadows = Cesium.ShadowMode.ENABLED;
viewer.scene.primitives.add(osmBuildings);

// ── ClippingPolygon: ~300 × 300 m footprint ──────────────────────────
// Buildings inside are clipped to simulate a cleared redevelopment site.
const dLon = 0.002;
const dLat = 0.0014;
const footprintPositions = Cesium.Cartesian3.fromDegreesArray([
CENTER_LON - dLon,
CENTER_LAT - dLat,
CENTER_LON + dLon,
CENTER_LAT - dLat,
CENTER_LON + dLon,
CENTER_LAT + dLat,
CENTER_LON - dLon,
CENTER_LAT + dLat,
]);

const clippingPolygons = new Cesium.ClippingPolygonCollection({
polygons: [new Cesium.ClippingPolygon({ positions: footprintPositions })],
});
osmBuildings.clippingPolygons = clippingPolygons;

// ── WoodTower — new building placed in the cleared area ───────────────
const tower = viewer.entities.add({
name: "Wood Tower",
position: Cesium.Cartesian3.fromDegrees(CENTER_LON, CENTER_LAT),
model: {
uri: "../../SampleData/models/WoodTower/Wood_Tower.glb",
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
shadows: Cesium.ShadowMode.ENABLED,
},
});

// ── CesiumAir with ClippingPlane — mirrors original bug report ───────
// The rear half of the aircraft is clipped. Without the fix, the
// missing half still casts a full shadow on the ground below.
const airClippingPlanes = new Cesium.ClippingPlaneCollection({
planes: [new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, 1.0, 0.0), 0.0)],
edgeWidth: 1.0,
edgeColor: Cesium.Color.YELLOW,
});

const airplane = viewer.entities.add({

Check failure on line 112 in Apps/Sandcastle/gallery/Shadows Clipping Polygon.html

View workflow job for this annotation

GitHub Actions / lint

'airplane' is assigned a value but never used
name: "CesiumAir",
position: Cesium.Cartesian3.fromDegrees(
CENTER_LON + dLon + 0.002,
CENTER_LAT + 0.001,
560.0,
),
model: {
uri: "../../SampleData/models/CesiumAir/Cesium_Air.glb",
minimumPixelSize: 64,
shadows: Cesium.ShadowMode.ENABLED,
clippingPlanes: airClippingPlanes,
},
});

// ── Camera — east of the scene, looking west (lake is to the south) ──
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(
CENTER_LON + 0.022,
CENTER_LAT + 0.004,
650.0,
),
orientation: {
heading: Cesium.Math.toRadians(255.0),
pitch: Cesium.Math.toRadians(-22.0),
roll: 0.0,
},
});

// ── Toolbar ──────────────────────────────────────────────────────────
Sandcastle.addToggleButton("Shadows", true, function (checked) {

Check failure on line 142 in Apps/Sandcastle/gallery/Shadows Clipping Polygon.html

View workflow job for this annotation

GitHub Actions / lint

'Sandcastle' is not defined
viewer.shadows = checked;
});

Sandcastle.addToggleButton("Clipping", true, function (checked) {

Check failure on line 146 in Apps/Sandcastle/gallery/Shadows Clipping Polygon.html

View workflow job for this annotation

GitHub Actions / lint

'Sandcastle' is not defined
clippingPolygons.enabled = checked;
airClippingPlanes.enabled = checked;
tower.show = checked;
});

Sandcastle.addToggleButton("Soft Shadows", false, function (checked) {

Check failure on line 152 in Apps/Sandcastle/gallery/Shadows Clipping Polygon.html

View workflow job for this annotation

GitHub Actions / lint

'Sandcastle' is not defined
shadowMap.softShadows = checked;
});

//Sandcastle_End
};

if (typeof Cesium !== "undefined") {
window.startupCalled = true;
window.startup(Cesium).catch((error) => {

Check failure on line 161 in Apps/Sandcastle/gallery/Shadows Clipping Polygon.html

View workflow job for this annotation

GitHub Actions / lint

'Cesium' is not defined
"use strict";

Check failure on line 162 in Apps/Sandcastle/gallery/Shadows Clipping Polygon.html

View workflow job for this annotation

GitHub Actions / lint

'use strict' is unnecessary inside of modules
console.error(error);
});
Sandcastle.finishedLoading();

Check failure on line 165 in Apps/Sandcastle/gallery/Shadows Clipping Polygon.html

View workflow job for this annotation

GitHub Actions / lint

'Sandcastle' is not defined
}
</script>
</body>
</html>
22 changes: 3 additions & 19 deletions packages/engine/Source/Scene/Model/Model.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,6 @@ function Model(options) {
this._clippingPlanes = clippingPlanes;
}
this._clippingPlanesState = 0; // If this value changes, the shaders need to be regenerated.
this._clippingPlanesMatrix = Matrix4.clone(Matrix4.IDENTITY); // Derived from reference matrix and the current view matrix

// If the given clipping polygons don't have an owner, make this model its owner.
// Otherwise, the clipping polygons are passed down from a tileset.
Expand Down Expand Up @@ -1870,7 +1869,6 @@ Model.prototype.resetDrawCommands = function () {

const scratchIBLReferenceFrameMatrix4 = new Matrix4();
const scratchIBLReferenceFrameMatrix3 = new Matrix3();
const scratchClippingPlanesMatrix = new Matrix4();

/**
* Called when {@link Viewer} or {@link CesiumWidget} render the scene to
Expand Down Expand Up @@ -2443,23 +2441,9 @@ function updateReferenceMatrices(model, frameState) {
model._iblReferenceFrameMatrix,
);

if (model.isClippingEnabled()) {
let clippingPlanesMatrix = scratchClippingPlanesMatrix;
clippingPlanesMatrix = Matrix4.multiply(
context.uniformState.view3D,
referenceMatrix,
clippingPlanesMatrix,
);
clippingPlanesMatrix = Matrix4.multiply(
clippingPlanesMatrix,
model._clippingPlanes.modelMatrix,
clippingPlanesMatrix,
);
model._clippingPlanesMatrix = Matrix4.inverseTranspose(
clippingPlanesMatrix,
model._clippingPlanesMatrix,
);
}
// model_clippingPlanesMatrix is now recomputed per draw call inside
// ModelClippingPlanesPipelineStage so that it uses the correct czm_view
// in both the main render pass (camera) and the shadow cast pass (light).
}

function updateSceneGraph(model, frameState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Cartesian2 from "../../Core/Cartesian2.js";
import ClippingPlaneCollection from "../ClippingPlaneCollection.js";
import combine from "../../Core/combine.js";
import Color from "../../Core/Color.js";
import defined from "../../Core/defined.js";
import Matrix4 from "../../Core/Matrix4.js";
import ModelClippingPlanesStageFS from "../../Shaders/Model/ModelClippingPlanesStageFS.js";
import ShaderDestination from "../../Renderer/ShaderDestination.js";

Expand All @@ -17,6 +19,9 @@ const ModelClippingPlanesPipelineStage = {
};

const textureResolutionScratch = new Cartesian2();
const scratchClippingPlanesMatrix = new Matrix4();
const scratchClippingPlanesMatrix2 = new Matrix4();

/**
* Process a model. This modifies the following parts of the render resources:
*
Expand Down Expand Up @@ -116,7 +121,26 @@ ModelClippingPlanesPipelineStage.process = function (
return style;
},
model_clippingPlanesMatrix: function () {
return model._clippingPlanesMatrix;
// Recompute each draw call using the current view matrix so this
// uniform is correct in the shadow cast pass too, where
// context.uniformState.view3D is the light's view, not the camera's.
const modelMatrix = defined(model._clampedModelMatrix)
? model._clampedModelMatrix
: model.modelMatrix;
const referenceMatrix = defined(model.referenceMatrix)
? model.referenceMatrix
: modelMatrix;
let m = Matrix4.multiply(
context.uniformState.view3D,
referenceMatrix,
scratchClippingPlanesMatrix,
);
m = Matrix4.multiply(
m,
clippingPlanes.modelMatrix,
scratchClippingPlanesMatrix2,
);
return Matrix4.inverseTranspose(m, scratchClippingPlanesMatrix);
},
};

Expand Down
12 changes: 9 additions & 3 deletions packages/engine/Source/Scene/ShadowMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -1719,20 +1719,25 @@ function createCastDerivedCommand(
const isPointLight = shadowMap._isPointLight;
const usesDepthTexture = shadowMap._usesDepthTexture;

const vertexShaderSource = shaderProgram.vertexShaderSource;
const fragmentShaderSource = shaderProgram.fragmentShaderSource;

const hasDiscard =
isOpaque &&
ShadowMapShader.containsDiscardForShadowCast(fragmentShaderSource);

const keyword = ShadowMapShader.getShadowCastShaderKeyword(
isPointLight,
isTerrain,
usesDepthTexture,
isOpaque,
hasDiscard,
);
castShader = context.shaderCache.getDerivedShaderProgram(
shaderProgram,
keyword,
);
if (!defined(castShader)) {
const vertexShaderSource = shaderProgram.vertexShaderSource;
const fragmentShaderSource = shaderProgram.fragmentShaderSource;

const castVS = ShadowMapShader.createShadowCastVertexShader(
vertexShaderSource,
isPointLight,
Expand All @@ -1743,6 +1748,7 @@ function createCastDerivedCommand(
isPointLight,
usesDepthTexture,
isOpaque,
hasDiscard,
);

castShader = context.shaderCache.createDerivedShaderProgram(
Expand Down
Loading
Loading