Skip to content

Commit ae07bb0

Browse files
authored
Merge pull request #12629 from CesiumGS/voxel-orthographic
Fix voxel rendering with orthographic camera
2 parents 2640bd8 + 9bce0d9 commit ae07bb0

File tree

8 files changed

+307
-23
lines changed

8 files changed

+307
-23
lines changed

Apps/Sandcastle/gallery/Voxels.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
geocoder: false,
4343
animation: false,
4444
timeline: false,
45+
projectionPicker: true,
46+
sceneModePicker: false,
4547
});
4648

4749
viewer.extend(Cesium.viewerVoxelInspectorMixin);

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
- Support Texture3D and add Volume Cloud SandBox Sample. [#12550](https://github.com/CesiumGS/cesium/issues/12550)
1010

11+
#### Fixes :wrench:
12+
13+
- Fixed voxel rendering with orthographic cameras. [#12629](https://github.com/CesiumGS/cesium/pull/12629)
14+
1115
## 1.129 - 2025-05-01
1216

1317
### @cesium/engine

Specs/e2e/voxel-cameras.spec.js

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import { test, expect } from "./test.js";
2+
3+
test("renders procedural voxel in perspective camera", async ({
4+
cesiumPage,
5+
}) => {
6+
await cesiumPage.goto();
7+
8+
await cesiumPage.page.evaluate(() => {
9+
const viewer = new Cesium.Viewer("cesiumContainer", {
10+
baseLayer: Cesium.ImageryLayer.fromProviderAsync(
11+
Cesium.TileMapServiceImageryProvider.fromUrl(
12+
Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII"),
13+
),
14+
),
15+
baseLayerPicker: false,
16+
geocoder: false,
17+
animation: false,
18+
timeline: false,
19+
sceneModePicker: false,
20+
homeButton: false,
21+
navigationHelpButton: false,
22+
});
23+
24+
const { scene, camera } = viewer;
25+
camera.setView({
26+
destination: new Cesium.Cartesian3(
27+
20463166.456674013,
28+
24169216.80790143,
29+
15536221.507601531,
30+
),
31+
orientation: new Cesium.HeadingPitchRoll(
32+
6.283185307179586,
33+
-1.5680902263173198,
34+
0,
35+
),
36+
});
37+
38+
const globalTransform = Cesium.Matrix4.fromScale(
39+
Cesium.Cartesian3.fromElements(
40+
Cesium.Ellipsoid.WGS84.maximumRadius,
41+
Cesium.Ellipsoid.WGS84.maximumRadius,
42+
Cesium.Ellipsoid.WGS84.maximumRadius,
43+
),
44+
);
45+
46+
function ProceduralSingleTileVoxelProvider(shape) {
47+
this.shape = shape;
48+
this.minBounds = Cesium.VoxelShapeType.getMinBounds(shape).clone();
49+
this.maxBounds = Cesium.VoxelShapeType.getMaxBounds(shape).clone();
50+
this.dimensions = new Cesium.Cartesian3(8, 8, 8);
51+
this.names = ["color"];
52+
this.types = [Cesium.MetadataType.VEC4];
53+
this.componentTypes = [Cesium.MetadataComponentType.FLOAT32];
54+
this.globalTransform = globalTransform;
55+
}
56+
57+
const scratchColor = new Cesium.Color();
58+
59+
ProceduralSingleTileVoxelProvider.prototype.requestData = function (
60+
options,
61+
) {
62+
if (options.tileLevel >= 1) {
63+
return Promise.reject(`No tiles available beyond level 0`);
64+
}
65+
66+
const dimensions = this.dimensions;
67+
const voxelCount = dimensions.x * dimensions.y * dimensions.z;
68+
const type = this.types[0];
69+
const channelCount = Cesium.MetadataType.getComponentCount(type);
70+
const dataColor = new Float32Array(voxelCount * channelCount);
71+
72+
const randomSeed = dimensions.y * dimensions.x + dimensions.x;
73+
Cesium.Math.setRandomNumberSeed(randomSeed);
74+
const hue = Cesium.Math.nextRandomNumber();
75+
76+
for (let z = 0; z < dimensions.z; z++) {
77+
for (let y = 0; y < dimensions.y; y++) {
78+
const indexZY = z * dimensions.y * dimensions.x + y * dimensions.x;
79+
for (let x = 0; x < dimensions.x; x++) {
80+
const lerperX = x / (dimensions.x - 1);
81+
const lerperY = y / (dimensions.y - 1);
82+
const lerperZ = z / (dimensions.z - 1);
83+
84+
const h = hue + lerperX * 0.5 - lerperY * 0.3 + lerperZ * 0.2;
85+
const s = 1.0 - lerperY * 0.2;
86+
const v = 0.5 + 2.0 * (lerperZ - 0.5) * 0.2;
87+
const color = Cesium.Color.fromHsl(h, s, v, 1.0, scratchColor);
88+
89+
const index = (indexZY + x) * channelCount;
90+
dataColor[index + 0] = color.red;
91+
dataColor[index + 1] = color.green;
92+
dataColor[index + 2] = color.blue;
93+
dataColor[index + 3] = 0.75;
94+
}
95+
}
96+
}
97+
98+
const content = Cesium.VoxelContent.fromMetadataArray([dataColor]);
99+
return Promise.resolve(content);
100+
};
101+
102+
const provider = new ProceduralSingleTileVoxelProvider(
103+
Cesium.VoxelShapeType.BOX,
104+
);
105+
106+
const customShader = new Cesium.CustomShader({
107+
fragmentShaderText: `void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
108+
{
109+
material.diffuse = fsInput.metadata.color.rgb;
110+
float transparency = 1.0 - fsInput.metadata.color.a;
111+
112+
// To mimic light scattering, use exponential decay
113+
float thickness = fsInput.voxel.travelDistance * 16.0;
114+
material.alpha = 1.0 - pow(transparency, thickness);
115+
}`,
116+
});
117+
118+
scene.primitives.add(new Cesium.VoxelPrimitive({ provider, customShader }));
119+
});
120+
await cesiumPage.page.clock.pauseAt(new Date("2023-12-25T14:00:00"));
121+
await cesiumPage.page.waitForLoadState("networkidle");
122+
123+
await cesiumPage.page.clock.runFor(1000);
124+
125+
await expect(cesiumPage.page).toHaveScreenshot();
126+
});
127+
128+
test("renders procedural voxel in orthographic camera", async ({
129+
cesiumPage,
130+
}) => {
131+
await cesiumPage.goto();
132+
133+
await cesiumPage.page.evaluate(() => {
134+
const viewer = new Cesium.Viewer("cesiumContainer", {
135+
baseLayer: Cesium.ImageryLayer.fromProviderAsync(
136+
Cesium.TileMapServiceImageryProvider.fromUrl(
137+
Cesium.buildModuleUrl("Assets/Textures/NaturalEarthII"),
138+
),
139+
),
140+
baseLayerPicker: false,
141+
geocoder: false,
142+
animation: false,
143+
timeline: false,
144+
sceneModePicker: false,
145+
homeButton: false,
146+
navigationHelpButton: false,
147+
});
148+
149+
const { scene, camera } = viewer;
150+
camera.setView({
151+
destination: new Cesium.Cartesian3(
152+
20463166.456674013,
153+
24169216.80790143,
154+
15536221.507601531,
155+
),
156+
orientation: new Cesium.HeadingPitchRoll(
157+
6.283185307179586,
158+
-1.5680902263173198,
159+
0,
160+
),
161+
});
162+
camera.switchToOrthographicFrustum();
163+
164+
const globalTransform = Cesium.Matrix4.fromScale(
165+
Cesium.Cartesian3.fromElements(
166+
Cesium.Ellipsoid.WGS84.maximumRadius,
167+
Cesium.Ellipsoid.WGS84.maximumRadius,
168+
Cesium.Ellipsoid.WGS84.maximumRadius,
169+
),
170+
);
171+
172+
function ProceduralSingleTileVoxelProvider(shape) {
173+
this.shape = shape;
174+
this.minBounds = Cesium.VoxelShapeType.getMinBounds(shape).clone();
175+
this.maxBounds = Cesium.VoxelShapeType.getMaxBounds(shape).clone();
176+
this.dimensions = new Cesium.Cartesian3(8, 8, 8);
177+
this.names = ["color"];
178+
this.types = [Cesium.MetadataType.VEC4];
179+
this.componentTypes = [Cesium.MetadataComponentType.FLOAT32];
180+
this.globalTransform = globalTransform;
181+
}
182+
183+
const scratchColor = new Cesium.Color();
184+
185+
ProceduralSingleTileVoxelProvider.prototype.requestData = function (
186+
options,
187+
) {
188+
if (options.tileLevel >= 1) {
189+
return Promise.reject(`No tiles available beyond level 0`);
190+
}
191+
192+
const dimensions = this.dimensions;
193+
const voxelCount = dimensions.x * dimensions.y * dimensions.z;
194+
const type = this.types[0];
195+
const channelCount = Cesium.MetadataType.getComponentCount(type);
196+
const dataColor = new Float32Array(voxelCount * channelCount);
197+
198+
const randomSeed = dimensions.y * dimensions.x + dimensions.x;
199+
Cesium.Math.setRandomNumberSeed(randomSeed);
200+
const hue = Cesium.Math.nextRandomNumber();
201+
202+
for (let z = 0; z < dimensions.z; z++) {
203+
for (let y = 0; y < dimensions.y; y++) {
204+
const indexZY = z * dimensions.y * dimensions.x + y * dimensions.x;
205+
for (let x = 0; x < dimensions.x; x++) {
206+
const lerperX = x / (dimensions.x - 1);
207+
const lerperY = y / (dimensions.y - 1);
208+
const lerperZ = z / (dimensions.z - 1);
209+
210+
const h = hue + lerperX * 0.5 - lerperY * 0.3 + lerperZ * 0.2;
211+
const s = 1.0 - lerperY * 0.2;
212+
const v = 0.5 + 2.0 * (lerperZ - 0.5) * 0.2;
213+
const color = Cesium.Color.fromHsl(h, s, v, 1.0, scratchColor);
214+
215+
const index = (indexZY + x) * channelCount;
216+
dataColor[index + 0] = color.red;
217+
dataColor[index + 1] = color.green;
218+
dataColor[index + 2] = color.blue;
219+
dataColor[index + 3] = 0.75;
220+
}
221+
}
222+
}
223+
224+
const content = Cesium.VoxelContent.fromMetadataArray([dataColor]);
225+
return Promise.resolve(content);
226+
};
227+
228+
const provider = new ProceduralSingleTileVoxelProvider(
229+
Cesium.VoxelShapeType.BOX,
230+
);
231+
232+
const customShader = new Cesium.CustomShader({
233+
fragmentShaderText: `void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material)
234+
{
235+
material.diffuse = fsInput.metadata.color.rgb;
236+
float transparency = 1.0 - fsInput.metadata.color.a;
237+
238+
// To mimic light scattering, use exponential decay
239+
float thickness = fsInput.voxel.travelDistance * 16.0;
240+
material.alpha = 1.0 - pow(transparency, thickness);
241+
}`,
242+
});
243+
244+
scene.primitives.add(new Cesium.VoxelPrimitive({ provider, customShader }));
245+
});
246+
await cesiumPage.page.clock.pauseAt(new Date("2023-12-25T14:00:00"));
247+
await cesiumPage.page.waitForLoadState("networkidle");
248+
249+
await cesiumPage.page.clock.runFor(1000);
250+
251+
await expect(cesiumPage.page).toHaveScreenshot();
252+
});

packages/engine/Source/Scene/VoxelPrimitive.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,12 @@ function VoxelPrimitive(options) {
338338
*/
339339
this._transformPositionWorldToUv = new Matrix4();
340340

341+
/**
342+
* @type {Matrix3}
343+
* @private
344+
*/
345+
this._transformDirectionWorldToUv = new Matrix3();
346+
341347
/**
342348
* @type {Matrix4}
343349
* @private
@@ -444,6 +450,7 @@ function VoxelPrimitive(options) {
444450
transformDirectionViewToLocal: new Matrix3(),
445451
transformNormalLocalToWorld: new Matrix3(),
446452
cameraPositionUv: new Cartesian3(),
453+
cameraDirectionUv: new Cartesian3(),
447454
ndcSpaceAxisAlignedBoundingBox: new Cartesian4(),
448455
clippingPlanesTexture: undefined,
449456
clippingPlanesMatrix: new Matrix4(),
@@ -1310,12 +1317,20 @@ VoxelPrimitive.prototype.update = function (frameState) {
13101317
this._transformNormalLocalToWorld,
13111318
uniforms.transformNormalLocalToWorld,
13121319
);
1313-
const cameraPositionWorld = frameState.camera.positionWC;
13141320
uniforms.cameraPositionUv = Matrix4.multiplyByPoint(
13151321
this._transformPositionWorldToUv,
1316-
cameraPositionWorld,
1322+
frameState.camera.positionWC,
13171323
uniforms.cameraPositionUv,
13181324
);
1325+
uniforms.cameraDirectionUv = Matrix3.multiplyByVector(
1326+
this._transformDirectionWorldToUv,
1327+
frameState.camera.directionWC,
1328+
uniforms.cameraDirectionUv,
1329+
);
1330+
uniforms.cameraDirectionUv = Cartesian3.normalize(
1331+
uniforms.cameraDirectionUv,
1332+
uniforms.cameraDirectionUv,
1333+
);
13191334
uniforms.stepSize = this._stepSizeMultiplier;
13201335

13211336
// Render the primitive
@@ -1630,6 +1645,10 @@ function updateShapeAndTransforms(primitive, shape, provider) {
16301645
transformPositionWorldToLocal,
16311646
primitive._transformPositionWorldToUv,
16321647
);
1648+
primitive._transformDirectionWorldToUv = Matrix4.getMatrix3(
1649+
primitive._transformPositionWorldToUv,
1650+
primitive._transformDirectionWorldToUv,
1651+
);
16331652
primitive._transformPositionUvToWorld = Matrix4.multiplyTransformation(
16341653
transformPositionLocalToWorld,
16351654
transformPositionUvToLocal,

packages/engine/Source/Scene/processVoxelProperties.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ function processVoxelProperties(renderResources, primitive) {
145145
shaderBuilder.addStructField(voxelStructId, "vec3", "positionShapeUv");
146146
shaderBuilder.addStructField(voxelStructId, "vec3", "positionUvLocal");
147147
shaderBuilder.addStructField(voxelStructId, "vec3", "viewDirUv");
148-
shaderBuilder.addStructField(voxelStructId, "vec3", "viewDirWorld");
149148
shaderBuilder.addStructField(voxelStructId, "vec3", "surfaceNormal");
150149
shaderBuilder.addStructField(voxelStructId, "float", "travelDistance");
151150
shaderBuilder.addStructField(voxelStructId, "int", "stepCount");

packages/engine/Source/Shaders/Builtin/Functions/windowToEyeCoordinates.glsl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ vec4 czm_screenToEyeCoordinates(vec2 screenCoordinateXY, float depthOrLogDepth)
7676
vec4 screenCoord = vec4(screenCoordinateXY, far * (1.0 - near / depthFromCamera) / (far - near), 1.0);
7777
vec4 eyeCoordinate = czm_screenToEyeCoordinates(screenCoord);
7878
eyeCoordinate.w = 1.0 / depthFromCamera; // Better precision
79-
return eyeCoordinate;
8079
#else
8180
vec4 screenCoord = vec4(screenCoordinateXY, depthOrLogDepth, 1.0);
8281
vec4 eyeCoordinate = czm_screenToEyeCoordinates(screenCoord);

0 commit comments

Comments
 (0)