You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1337 lines
41 KiB
JavaScript

import { BoundingSphere } from "../../Source/Cesium.js";
import { BoxGeometry } from "../../Source/Cesium.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { Color } from "../../Source/Cesium.js";
import { ColorGeometryInstanceAttribute } from "../../Source/Cesium.js";
import { ComponentDatatype } from "../../Source/Cesium.js";
import { EllipsoidTerrainProvider } from "../../Source/Cesium.js";
import { GeometryInstance } from "../../Source/Cesium.js";
import { HeadingPitchRange } from "../../Source/Cesium.js";
import { HeadingPitchRoll } from "../../Source/Cesium.js";
import { HeightmapTerrainData } from "../../Source/Cesium.js";
import { JulianDate } from "../../Source/Cesium.js";
import { Math as CesiumMath } from "../../Source/Cesium.js";
import { OrthographicOffCenterFrustum } from "../../Source/Cesium.js";
import { PixelFormat } from "../../Source/Cesium.js";
import { Transforms } from "../../Source/Cesium.js";
import { WebGLConstants } from "../../Source/Cesium.js";
import { Context } from "../../Source/Cesium.js";
import { Framebuffer } from "../../Source/Cesium.js";
import { PixelDatatype } from "../../Source/Cesium.js";
import { Texture } from "../../Source/Cesium.js";
import { Camera } from "../../Source/Cesium.js";
import { DirectionalLight } from "../../Source/Cesium.js";
import { Globe } from "../../Source/Cesium.js";
import { Model } from "../../Source/Cesium.js";
import { PerInstanceColorAppearance } from "../../Source/Cesium.js";
import { Primitive } from "../../Source/Cesium.js";
import { ShadowMap } from "../../Source/Cesium.js";
import { ShadowMode } from "../../Source/Cesium.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";
import { when } from "../../Source/Cesium.js";
describe(
"Scene/ShadowMap",
function () {
var scene;
var sunShadowMap;
var backgroundColor = [0, 0, 0, 255];
var longitude = -1.31968;
var latitude = 0.4101524;
var height = 0.0;
var boxHeight = 4.0;
var floorHeight = -1.0;
var boxUrl = "./Data/Models/Shadows/Box.gltf";
var boxTranslucentUrl = "./Data/Models/Shadows/BoxTranslucent.gltf";
var boxCutoutUrl = "./Data/Models/Shadows/BoxCutout.gltf";
var boxInvertedUrl = "./Data/Models/Shadows/BoxInverted.gltf";
var box;
var boxTranslucent;
var boxCutout;
var room;
var floor;
var floorTranslucent;
var primitiveBox;
var primitiveBoxRTC;
var primitiveBoxTranslucent;
var primitiveFloor;
var primitiveFloorRTC;
beforeAll(function () {
scene = createScene();
scene.frameState.scene3DOnly = true;
Color.unpack(backgroundColor, 0, scene.backgroundColor);
sunShadowMap = scene.shadowMap;
var boxOrigin = new Cartesian3.fromRadians(
longitude,
latitude,
boxHeight
);
var boxTransform = Transforms.headingPitchRollToFixedFrame(
boxOrigin,
new HeadingPitchRoll()
);
var floorOrigin = new Cartesian3.fromRadians(
longitude,
latitude,
floorHeight
);
var floorTransform = Transforms.headingPitchRollToFixedFrame(
floorOrigin,
new HeadingPitchRoll()
);
var roomOrigin = new Cartesian3.fromRadians(longitude, latitude, height);
var roomTransform = Transforms.headingPitchRollToFixedFrame(
roomOrigin,
new HeadingPitchRoll()
);
var modelPromises = [];
modelPromises.push(
loadModel({
url: boxUrl,
modelMatrix: boxTransform,
scale: 0.5,
show: false,
}).then(function (model) {
box = model;
})
);
modelPromises.push(
loadModel({
url: boxTranslucentUrl,
modelMatrix: boxTransform,
scale: 0.5,
show: false,
}).then(function (model) {
boxTranslucent = model;
})
);
modelPromises.push(
loadModel({
url: boxCutoutUrl,
modelMatrix: boxTransform,
scale: 0.5,
incrementallyLoadTextures: false,
show: false,
}).then(function (model) {
boxCutout = model;
})
);
modelPromises.push(
loadModel({
url: boxUrl,
modelMatrix: floorTransform,
scale: 2.0,
show: false,
}).then(function (model) {
floor = model;
})
);
modelPromises.push(
loadModel({
url: boxTranslucentUrl,
modelMatrix: floorTransform,
scale: 2.0,
show: false,
}).then(function (model) {
floorTranslucent = model;
})
);
modelPromises.push(
loadModel({
url: boxInvertedUrl,
modelMatrix: roomTransform,
scale: 8.0,
show: false,
}).then(function (model) {
room = model;
})
);
primitiveBox = createPrimitive(boxTransform, 0.5, Color.RED);
primitiveBoxRTC = createPrimitiveRTC(boxTransform, 0.5, Color.RED);
primitiveBoxTranslucent = createPrimitive(
boxTransform,
0.5,
Color.RED.withAlpha(0.5)
);
primitiveFloor = createPrimitive(floorTransform, 2.0, Color.RED);
primitiveFloorRTC = createPrimitiveRTC(floorTransform, 2.0, Color.RED);
return when.all(modelPromises);
});
function createPrimitive(transform, size, color) {
return scene.primitives.add(
new Primitive({
geometryInstances: new GeometryInstance({
geometry: BoxGeometry.fromDimensions({
dimensions: new Cartesian3(size, size, size),
vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
}),
modelMatrix: transform,
attributes: {
color: ColorGeometryInstanceAttribute.fromColor(color),
},
}),
appearance: new PerInstanceColorAppearance({
translucent: false,
closed: true,
}),
asynchronous: false,
show: false,
shadows: ShadowMode.ENABLED,
})
);
}
function createPrimitiveRTC(transform, size, color) {
var boxGeometry = BoxGeometry.createGeometry(
BoxGeometry.fromDimensions({
vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
dimensions: new Cartesian3(size, size, size),
})
);
var positions = boxGeometry.attributes.position.values;
var newPositions = new Float32Array(positions.length);
for (var i = 0; i < positions.length; ++i) {
newPositions[i] = positions[i];
}
boxGeometry.attributes.position.values = newPositions;
boxGeometry.attributes.position.componentDatatype =
ComponentDatatype.FLOAT;
BoundingSphere.transform(
boxGeometry.boundingSphere,
transform,
boxGeometry.boundingSphere
);
var boxGeometryInstance = new GeometryInstance({
geometry: boxGeometry,
attributes: {
color: ColorGeometryInstanceAttribute.fromColor(color),
},
});
return scene.primitives.add(
new Primitive({
geometryInstances: boxGeometryInstance,
appearance: new PerInstanceColorAppearance({
translucent: false,
closed: true,
}),
asynchronous: false,
rtcCenter: boxGeometry.boundingSphere.center,
show: false,
shadows: ShadowMode.ENABLED,
})
);
}
function loadModel(options) {
var model = scene.primitives.add(Model.fromGltf(options));
return pollToPromise(
function () {
// Render scene to progressively load the model
scene.render();
return model.ready;
},
{ timeout: 10000 }
).then(function () {
return model;
});
}
/**
* Repeatedly calls render until the load queue is empty. Returns a promise that resolves
* when the load queue is empty.
*/
function loadGlobe() {
return pollToPromise(function () {
scene.render();
var globe = scene.globe;
return (
globe._surface.tileProvider.ready &&
globe._surface._tileLoadQueueHigh.length === 0 &&
globe._surface._tileLoadQueueMedium.length === 0 &&
globe._surface._tileLoadQueueLow.length === 0 &&
globe._surface._debug.tilesWaitingForChildren === 0
);
});
}
afterAll(function () {
scene.destroyForSpecs();
});
afterEach(function () {
var length = scene.primitives.length;
for (var i = 0; i < length; ++i) {
scene.primitives.get(i).show = false;
}
scene.globe = undefined;
scene.shadowMap = scene.shadowMap && scene.shadowMap.destroy();
});
function createCascadedShadowMap() {
var center = new Cartesian3.fromRadians(longitude, latitude, height);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
// Create light camera pointing straight down
var lightCamera = new Camera(scene);
lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 1.0));
scene.shadowMap = new ShadowMap({
context: scene.context,
lightCamera: lightCamera,
});
}
function createSingleCascadeShadowMap() {
var center = new Cartesian3.fromRadians(longitude, latitude, height);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
// Create light camera pointing straight down
var lightCamera = new Camera(scene);
lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 1.0));
scene.shadowMap = new ShadowMap({
context: scene.context,
lightCamera: lightCamera,
numberOfCascades: 1,
});
}
function createShadowMapForDirectionalLight() {
var center = new Cartesian3.fromRadians(longitude, latitude, height);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
var frustum = new OrthographicOffCenterFrustum();
frustum.left = -50.0;
frustum.right = 50.0;
frustum.bottom = -50.0;
frustum.top = 50.0;
frustum.near = 1.0;
frustum.far = 1000;
// Create light camera pointing straight down
var lightCamera = new Camera(scene);
lightCamera.frustum = frustum;
lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 20.0));
scene.shadowMap = new ShadowMap({
context: scene.context,
lightCamera: lightCamera,
cascadesEnabled: false,
});
}
function createShadowMapForSpotLight() {
var center = new Cartesian3.fromRadians(longitude, latitude, height);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
var lightCamera = new Camera(scene);
lightCamera.frustum.fov = CesiumMath.PI_OVER_TWO;
lightCamera.frustum.aspectRatio = 1.0;
lightCamera.frustum.near = 1.0;
lightCamera.frustum.far = 1000.0;
lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 20.0));
scene.shadowMap = new ShadowMap({
context: scene.context,
lightCamera: lightCamera,
cascadesEnabled: false,
});
}
function createShadowMapForPointLight() {
var center = new Cartesian3.fromRadians(longitude, latitude, height);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
var lightCamera = new Camera(scene);
lightCamera.position = center;
scene.shadowMap = new ShadowMap({
context: scene.context,
lightCamera: lightCamera,
isPointLight: true,
});
}
function renderAndExpect(rgba, time) {
expect({
scene: scene,
time: time,
primeShadowMap: true,
}).toRender(rgba);
}
function renderAndReadPixels() {
var color;
expect({
scene: scene,
primeShadowMap: true,
}).toRenderAndCall(function (rgba) {
color = rgba;
});
return color;
}
function renderAndCall(expectationCallback, time) {
expect({
scene: scene,
time: time,
primeShadowMap: true,
}).toRenderAndCall(function (rgba) {
expectationCallback(rgba);
});
}
function verifyShadows(caster, receiver) {
caster.shadows = ShadowMode.ENABLED;
receiver.shadows = ShadowMode.ENABLED;
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor;
renderAndCall(function (rgba) {
unshadowedColor = rgba;
expect(unshadowedColor).not.toEqual(backgroundColor);
});
// Render with shadows
scene.shadowMap.enabled = true;
var shadowedColor;
renderAndCall(function (rgba) {
shadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
});
// Turn shadow casting off/on
caster.shadows = ShadowMode.DISABLED;
renderAndExpect(unshadowedColor);
caster.shadows = ShadowMode.ENABLED;
renderAndExpect(shadowedColor);
// Turn shadow receiving off/on
receiver.shadows = ShadowMode.DISABLED;
renderAndExpect(unshadowedColor);
receiver.shadows = ShadowMode.ENABLED;
renderAndExpect(shadowedColor);
// Move the camera away from the shadow
scene.camera.moveRight(0.5);
renderAndExpect(unshadowedColor);
}
it("sets default shadow map properties", function () {
scene.shadowMap = new ShadowMap({
context: scene.context,
lightCamera: new Camera(scene),
});
expect(scene.shadowMap.enabled).toBe(true);
expect(scene.shadowMap.softShadows).toBe(false);
expect(scene.shadowMap.isPointLight).toBe(false);
expect(scene.shadowMap._isSpotLight).toBe(false);
expect(scene.shadowMap._cascadesEnabled).toBe(true);
expect(scene.shadowMap._numberOfCascades).toBe(4);
expect(scene.shadowMap._normalOffset).toBe(true);
});
it("throws without options.context", function () {
expect(function () {
scene.shadowMap = new ShadowMap({
lightCamera: new Camera(scene),
});
}).toThrowDeveloperError();
});
it("throws without options.lightCamera", function () {
expect(function () {
scene.shadowMap = new ShadowMap({
context: scene.context,
});
}).toThrowDeveloperError();
});
it("throws when options.numberOfCascades is not one or four", function () {
expect(function () {
scene.shadowMap = new ShadowMap({
context: scene.context,
lightCamera: new Camera(scene),
numberOfCascades: 3,
});
}).toThrowDeveloperError();
});
it("model casts shadows onto another model", function () {
box.show = true;
floor.show = true;
createCascadedShadowMap();
verifyShadows(box, floor);
});
it("translucent model casts shadows onto another model", function () {
boxTranslucent.show = true;
floor.show = true;
createCascadedShadowMap();
verifyShadows(boxTranslucent, floor);
});
it("model with cutout texture casts shadows onto another model", function () {
boxCutout.show = true;
floor.show = true;
createCascadedShadowMap();
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor;
renderAndCall(function (rgba) {
unshadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
});
// Render with shadows. The area should not be shadowed because the box's texture is transparent in the center.
scene.shadowMap.enabled = true;
renderAndExpect(unshadowedColor);
// Move the camera into the shadowed area
scene.camera.moveRight(0.2);
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
});
// Move the camera away from the shadow
scene.camera.moveRight(0.3);
renderAndExpect(unshadowedColor);
});
it("primitive casts shadows onto another primitive", function () {
primitiveBox.show = true;
primitiveFloor.show = true;
createCascadedShadowMap();
verifyShadows(primitiveBox, primitiveFloor);
});
it("RTC primitive casts shadows onto another RTC primitive", function () {
primitiveBoxRTC.show = true;
primitiveFloorRTC.show = true;
createCascadedShadowMap();
verifyShadows(primitiveBoxRTC, primitiveFloorRTC);
});
it("translucent primitive casts shadows onto another primitive", function () {
primitiveBoxTranslucent.show = true;
primitiveFloor.show = true;
createCascadedShadowMap();
verifyShadows(primitiveBoxTranslucent, primitiveFloor);
});
it("model casts shadow onto globe", function () {
box.show = true;
scene.globe = new Globe();
scene.camera.frustum._sseDenominator = 0.005;
createCascadedShadowMap();
return loadGlobe().then(function () {
verifyShadows(box, scene.globe);
});
});
it("globe casts shadow onto globe", function () {
scene.globe = new Globe();
scene.camera.frustum._sseDenominator = 0.01;
var center = new Cartesian3.fromRadians(longitude, latitude, height);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
// Create light camera that is angled horizontally
var lightCamera = new Camera(scene);
lightCamera.lookAt(center, new Cartesian3(1.0, 0.0, 0.1));
scene.shadowMap = new ShadowMap({
context: scene.context,
lightCamera: lightCamera,
});
// Instead of the default flat tile, add a ridge that will cast shadows
spyOn(
EllipsoidTerrainProvider.prototype,
"requestTileGeometry"
).and.callFake(function () {
var width = 16;
var height = 16;
var buffer = new Uint8Array(width * height);
for (var i = 0; i < buffer.length; ++i) {
var row = i % width;
if (row > 6 && row < 10) {
buffer[i] = 1;
}
}
return new HeightmapTerrainData({
buffer: buffer,
width: width,
height: height,
});
});
return loadGlobe().then(function () {
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor;
renderAndCall(function (rgba) {
unshadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
});
// Render with globe casting off
scene.shadowMap.enabled = true;
scene.globe.shadows = ShadowMode.DISABLED;
renderAndExpect(unshadowedColor);
// Render with globe casting on
scene.globe.shadows = ShadowMode.ENABLED;
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
});
});
});
it("changes light direction", function () {
box.show = true;
floor.show = true;
var center = new Cartesian3.fromRadians(longitude, latitude, height);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
// Create light camera pointing straight down
var lightCamera = new Camera(scene);
lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 1.0));
scene.shadowMap = new ShadowMap({
context: scene.context,
lightCamera: lightCamera,
});
// Render with shadows
var shadowedColor = renderAndReadPixels();
// Move the camera away from the shadow
scene.camera.moveLeft(0.5);
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(shadowedColor);
});
// Change the light direction so the unshadowed area is now shadowed
lightCamera.lookAt(center, new Cartesian3(0.1, 0.0, 1.0));
renderAndExpect(shadowedColor);
});
it("sun shadow map works", function () {
box.show = true;
floor.show = true;
var startTime = new JulianDate(2457561.211806); // Sun pointing straight above
var endTime = new JulianDate(2457561.276389); // Sun at an angle
var center = new Cartesian3.fromRadians(longitude, latitude, height);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
// Use the default shadow map which uses the sun as a light source
scene.shadowMap = sunShadowMap;
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor;
renderAndCall(function (rgba) {
unshadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
});
// Render with shadows
scene.shadowMap.enabled = true;
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
}, startTime);
// Change the time so that the shadows are no longer pointing straight down
renderAndExpect(unshadowedColor, endTime);
scene.shadowMap = undefined;
});
it("uses scene's light source", function () {
var originalLight = scene.light;
box.show = true;
floor.show = true;
var lightDirectionAbove = new Cartesian3(
-0.22562675028973597,
0.8893549458095356,
-0.3976686433675793
); // Light pointing straight above
var lightDirectionAngle = new Cartesian3(
0.14370705890272903,
0.9062077731227641,
-0.3976628636840613
); // Light at an angle
var center = new Cartesian3.fromRadians(longitude, latitude, height);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
// Use the default shadow map which uses the scene's light source
scene.light = new DirectionalLight({
direction: lightDirectionAbove,
});
scene.shadowMap = sunShadowMap;
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor;
renderAndCall(function (rgba) {
unshadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
});
// Render with shadows
scene.shadowMap.enabled = true;
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
});
// Change the light so that the shadows are no longer pointing straight down
scene.light = new DirectionalLight({
direction: lightDirectionAngle,
});
renderAndExpect(unshadowedColor);
scene.shadowMap = undefined;
scene.light = originalLight;
});
it("single cascade shadow map", function () {
box.show = true;
floor.show = true;
createSingleCascadeShadowMap();
verifyShadows(box, floor);
});
it("directional shadow map", function () {
box.show = true;
floor.show = true;
createShadowMapForDirectionalLight();
verifyShadows(box, floor);
});
it("spot light shadow map", function () {
box.show = true;
floor.show = true;
createShadowMapForSpotLight();
verifyShadows(box, floor);
});
it("point light shadows", function () {
// Check that shadows are cast from all directions.
// Place the point light in the middle of an enclosed area and place a box on each side.
room.show = true;
createShadowMapForPointLight();
var longitudeSpacing = 0.0000003419296208325038;
var latitudeSpacing = 0.000000315782;
var heightSpacing = 2.0;
var origins = [
Cartesian3.fromRadians(longitude, latitude + latitudeSpacing, height),
Cartesian3.fromRadians(longitude, latitude - latitudeSpacing, height),
Cartesian3.fromRadians(longitude + longitudeSpacing, latitude, height),
Cartesian3.fromRadians(longitude - longitudeSpacing, latitude, height),
Cartesian3.fromRadians(longitude, latitude, height - heightSpacing),
Cartesian3.fromRadians(longitude, latitude, height + heightSpacing),
];
var offsets = [
new HeadingPitchRange(0.0, 0.0, 0.1),
new HeadingPitchRange(CesiumMath.PI, 0.0, 0.1),
new HeadingPitchRange(CesiumMath.PI_OVER_TWO, 0.0, 0.1),
new HeadingPitchRange(CesiumMath.THREE_PI_OVER_TWO, 0.0, 0.1),
new HeadingPitchRange(0, -CesiumMath.PI_OVER_TWO, 0.1),
new HeadingPitchRange(0, CesiumMath.PI_OVER_TWO, 0.1),
];
for (var i = 0; i < 6; ++i) {
var box = scene.primitives.add(
Model.fromGltf({
url: boxUrl,
modelMatrix: Transforms.headingPitchRollToFixedFrame(
origins[i],
new HeadingPitchRoll()
),
scale: 0.2,
})
);
scene.render(); // Model is pre-loaded, render one frame to make it ready
scene.camera.lookAt(origins[i], offsets[i]);
scene.camera.moveForward(0.5);
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor;
//eslint-disable-next-line no-loop-func
renderAndCall(function (rgba) {
unshadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
});
// Render with shadows
scene.shadowMap.enabled = true;
//eslint-disable-next-line no-loop-func
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
});
// Check that setting a smaller radius works
var radius = scene.shadowMap._pointLightRadius;
scene.shadowMap._pointLightRadius = 3.0;
renderAndExpect(unshadowedColor);
scene.shadowMap._pointLightRadius = radius;
// Move the camera away from the shadow
scene.camera.moveRight(0.5);
renderAndExpect(unshadowedColor);
scene.primitives.remove(box);
}
});
it("changes size", function () {
box.show = true;
floor.show = true;
createCascadedShadowMap();
// Render with shadows
var shadowedColor = renderAndReadPixels();
// Change size
scene.shadowMap.size = 256;
renderAndExpect(shadowedColor);
// Cascaded shadows combine four maps into one texture
expect(scene.shadowMap._shadowMapTexture.width).toBe(512);
expect(scene.shadowMap._shadowMapTexture.height).toBe(512);
expect(scene.shadowMap.size).toBe(256);
});
it("enable debugCascadeColors", function () {
box.show = true;
floor.show = true;
createCascadedShadowMap();
// Render with shadows
var shadowedColor = renderAndReadPixels();
// Render cascade colors
scene.shadowMap.debugCascadeColors = true;
expect(scene.shadowMap.dirty).toBe(true);
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(shadowedColor);
});
});
it("enable soft shadows", function () {
box.show = true;
floor.show = true;
createCascadedShadowMap();
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor = renderAndReadPixels();
// Render with shadows
scene.shadowMap.enabled = true;
expect(scene.shadowMap.dirty).toBe(true);
var shadowedColor = renderAndReadPixels();
// Render with soft shadows
scene.shadowMap.softShadows = true;
scene.shadowMap.size = 256; // Make resolution smaller to more easily verify soft edges
scene.camera.moveRight(0.25);
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
expect(rgba).not.toEqual(shadowedColor);
});
});
it("changes darkness", function () {
box.show = true;
floor.show = true;
createCascadedShadowMap();
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor = renderAndReadPixels();
// Render with shadows
scene.shadowMap.enabled = true;
var shadowedColor = renderAndReadPixels();
scene.shadowMap.darkness = 0.5;
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
expect(rgba).not.toEqual(shadowedColor);
});
});
function depthFramebufferSupported() {
var framebuffer = new Framebuffer({
context: scene.context,
depthStencilTexture: new Texture({
context: scene.context,
width: 1,
height: 1,
pixelFormat: PixelFormat.DEPTH_STENCIL,
pixelDatatype: PixelDatatype.UNSIGNED_INT_24_8,
}),
});
return framebuffer.status === WebGLConstants.FRAMEBUFFER_COMPLETE;
}
it("defaults to color texture if depth texture extension is not supported", function () {
box.show = true;
floor.show = true;
createCascadedShadowMap();
renderAndCall(function (rgba) {
if (scene.context.depthTexture) {
if (depthFramebufferSupported()) {
expect(scene.shadowMap._usesDepthTexture).toBe(true);
expect(scene.shadowMap._shadowMapTexture.pixelFormat).toEqual(
PixelFormat.DEPTH_STENCIL
);
} else {
// Depth texture extension is supported, but it fails to create create a depth-only FBO
expect(scene.shadowMap._usesDepthTexture).toBe(false);
expect(scene.shadowMap._shadowMapTexture.pixelFormat).toEqual(
PixelFormat.RGBA
);
}
}
});
scene.shadowMap = scene.shadowMap && scene.shadowMap.destroy();
// Disable extension
var depthTexture = scene.context._depthTexture;
scene.context._depthTexture = false;
createCascadedShadowMap();
renderAndCall(function (rgba) {
expect(scene.shadowMap._usesDepthTexture).toBe(false);
expect(scene.shadowMap._shadowMapTexture.pixelFormat).toEqual(
PixelFormat.RGBA
);
});
// Re-enable extension
scene.context._depthTexture = depthTexture;
});
it("does not render shadows when the camera is far away from any shadow receivers", function () {
box.show = true;
floor.show = true;
createCascadedShadowMap();
renderAndCall(function (rgba) {
expect(scene.shadowMap.outOfView).toBe(false);
});
var center = new Cartesian3.fromRadians(longitude, latitude, 200000);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
renderAndCall(function (rgba) {
expect(scene.shadowMap.outOfView).toBe(true);
});
});
it("does not render shadows when the light direction is below the horizon", function () {
box.show = true;
floor.show = true;
var center = new Cartesian3.fromRadians(longitude, latitude, height);
scene.camera.lookAt(
center,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
);
// Create light camera pointing straight down
var lightCamera = new Camera(scene);
lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 1.0));
scene.shadowMap = new ShadowMap({
context: scene.context,
lightCamera: lightCamera,
});
renderAndCall(function (rgba) {
expect(scene.shadowMap.outOfView).toBe(false);
});
// Change light direction
lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, -1.0));
renderAndCall(function (rgba) {
expect(scene.shadowMap.outOfView).toBe(true);
});
});
it("enable debugShow for cascaded shadow map", function () {
createCascadedShadowMap();
// Shadow overlay command, shadow volume outline, camera outline, four cascade outlines, four cascade planes
scene.shadowMap.debugShow = true;
scene.shadowMap.debugFreezeFrame = true;
renderAndCall(function (rgba) {
expect(scene.frameState.commandList.length).toBe(13);
});
scene.shadowMap.debugShow = false;
renderAndCall(function (rgba) {
expect(scene.frameState.commandList.length).toBe(0);
});
});
it("enable debugShow for fixed shadow map", function () {
createShadowMapForDirectionalLight();
// Overlay command, shadow volume outline, shadow volume planes
scene.shadowMap.debugShow = true;
renderAndCall(function (rgba) {
expect(scene.frameState.commandList.length).toBe(3);
});
scene.shadowMap.debugShow = false;
renderAndCall(function (rgba) {
expect(scene.frameState.commandList.length).toBe(0);
});
});
it("enable debugShow for point light shadow map", function () {
createShadowMapForPointLight();
// Overlay command and shadow volume outline
scene.shadowMap.debugShow = true;
renderAndCall(function (rgba) {
expect(scene.frameState.commandList.length).toBe(2);
});
scene.shadowMap.debugShow = false;
renderAndCall(function (rgba) {
expect(scene.frameState.commandList.length).toBe(0);
});
});
it("enable fitNearFar", function () {
box.show = true;
floor.show = true;
createShadowMapForDirectionalLight();
scene.shadowMap._fitNearFar = true; // True by default
var shadowNearFit;
var shadowFarFit;
renderAndCall(function (rgba) {
shadowNearFit = scene.shadowMap._sceneCamera.frustum.near;
shadowFarFit = scene.shadowMap._sceneCamera.frustum.far;
});
scene.shadowMap._fitNearFar = false;
renderAndCall(function (rgba) {
var shadowNear = scene.shadowMap._sceneCamera.frustum.near;
var shadowFar = scene.shadowMap._sceneCamera.frustum.far;
// When fitNearFar is true the shadowed region is smaller
expect(shadowNear).toBeLessThan(shadowNearFit);
expect(shadowFar).toBeGreaterThan(shadowFarFit);
});
});
it("set normalOffset", function () {
createCascadedShadowMap();
scene.shadowMap.normalOffset = false;
expect(scene.shadowMap._normalOffset, false);
expect(scene.shadowMap._terrainBias, false);
expect(scene.shadowMap._primitiveBias, false);
expect(scene.shadowMap._pointBias, false);
});
it("set maximumDistance", function () {
box.show = true;
floor.show = true;
createCascadedShadowMap();
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor;
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
unshadowedColor = rgba;
});
// Render with shadows
scene.shadowMap.enabled = true;
var shadowedColor;
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
});
// Set a maximum distance where the shadows start to fade out
scene.shadowMap.maximumDistance = 6.0;
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
expect(rgba).not.toEqual(shadowedColor);
});
// Set a maximimum distance where the shadows are not visible
scene.shadowMap.maximumDistance = 3.0;
renderAndExpect(unshadowedColor);
});
it("shadows are disabled during the pick pass", function () {
var spy = spyOn(Context.prototype, "draw").and.callThrough();
boxTranslucent.show = true;
floorTranslucent.show = true;
createCascadedShadowMap();
// Render normally and expect every model shader program to be shadow related.
renderAndCall(function (rgba) {
var count = spy.calls.count();
for (var i = 0; i < count; ++i) {
var drawCommand = spy.calls.argsFor(i)[0];
if (drawCommand.owner.primitive instanceof Model) {
expect(
drawCommand.shaderProgram._fragmentShaderText.indexOf(
"czm_shadow"
) !== -1
).toBe(true);
}
}
});
// Do the pick pass and expect every model shader program to not be shadow related. This also checks
// that there are no shadow cast commands.
spy.calls.reset();
expect(scene).toPickAndCall(function (result) {
var count = spy.calls.count();
for (var i = 0; i < count; ++i) {
var drawCommand = spy.calls.argsFor(i)[0];
if (drawCommand.owner.primitive instanceof Model) {
expect(
drawCommand.shaderProgram._fragmentShaderText.indexOf(
"czm_shadow"
) !== -1
).toBe(false);
}
}
});
});
it("model updates derived commands when the shadow map is dirty", function () {
var spy1 = spyOn(
ShadowMap,
"createReceiveDerivedCommand"
).and.callThrough();
var spy2 = spyOn(ShadowMap, "createCastDerivedCommand").and.callThrough();
box.show = true;
floor.show = true;
createCascadedShadowMap();
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor;
renderAndCall(function (rgba) {
unshadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
});
// Render with shadows
scene.shadowMap.enabled = true;
var shadowedColor;
renderAndCall(function (rgba) {
shadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
});
// Hide floor temporarily and change the shadow map
floor.show = false;
scene.shadowMap.debugCascadeColors = true;
// Render a few frames
var i;
for (i = 0; i < 6; ++i) {
scene.render();
}
// Show the floor and render. The receive shadows shader should now be up-to-date.
floor.show = true;
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
expect(rgba).not.toEqual(shadowedColor);
});
// Render a few more frames
for (i = 0; i < 6; ++i) {
scene.render();
}
// When using WebGL, this value is 8. When using the stub, this value is 4.
expect(spy1.calls.count()).toBeLessThanOrEqualTo(8);
expect(spy2.calls.count()).toEqual(4);
box.show = false;
floor.show = false;
});
it("does not receive shadows if fromLightSource is false", function () {
box.show = true;
floorTranslucent.show = true;
createCascadedShadowMap();
scene.shadowMap.fromLightSource = false;
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor;
renderAndCall(function (rgba) {
unshadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
});
// Render with shadows
scene.shadowMap.enabled = true;
renderAndCall(function (rgba) {
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).toEqual(unshadowedColor);
});
});
it("tweaking shadow bias parameters works", function () {
box.show = true;
floor.show = true;
createCascadedShadowMap();
// Render without shadows
scene.shadowMap.enabled = false;
var unshadowedColor;
renderAndCall(function (rgba) {
unshadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
});
// Render with shadows
scene.shadowMap.enabled = true;
var shadowedColor;
renderAndCall(function (rgba) {
shadowedColor = rgba;
expect(rgba).not.toEqual(backgroundColor);
expect(rgba).not.toEqual(unshadowedColor);
});
scene.shadowMap._primitiveBias.polygonOffsetFactor = 1.2;
scene.shadowMap._primitiveBias.polygonOffsetFactor = 4.1;
scene.shadowMap._primitiveBias.normalOffsetScale = 2.1;
scene.shadowMap._primitiveBias.normalShadingSmooth = 0.4;
scene.shadowMap.debugCreateRenderStates();
scene.shadowMap.dirty = true;
renderAndExpect(shadowedColor);
scene.shadowMap._primitiveBias.normalOffset = false;
scene.shadowMap._primitiveBias.normalShading = false;
scene.shadowMap._primitiveBias.polygonOffset = false;
scene.shadowMap.debugCreateRenderStates();
scene.shadowMap.dirty = true;
renderAndExpect(shadowedColor);
});
it("destroys", function () {
box.show = true;
floor.show = true;
createCascadedShadowMap();
expect(scene.shadowMap.isDestroyed()).toEqual(false);
scene.shadowMap.destroy();
expect(scene.shadowMap.isDestroyed()).toEqual(true);
scene.shadowMap = undefined;
});
},
"WebGL"
);