From 8dde73d3b19c1e3eb75534236ed62d905782db19 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 3 Apr 2025 11:59:20 -0400 Subject: [PATCH 01/13] Add support for cylinder voxels --- Content/Materials/CesiumVoxelTemplate.hlsl | 146 +++++++++++++++++- .../Private/CesiumVoxelMetadataComponent.cpp | 1 + .../Private/CesiumVoxelRendererComponent.cpp | 62 ++++++++ 3 files changed, 202 insertions(+), 7 deletions(-) diff --git a/Content/Materials/CesiumVoxelTemplate.hlsl b/Content/Materials/CesiumVoxelTemplate.hlsl index 81a64d494..1f9e7e349 100644 --- a/Content/Materials/CesiumVoxelTemplate.hlsl +++ b/Content/Materials/CesiumVoxelTemplate.hlsl @@ -415,6 +415,11 @@ struct ShapeUtility MinBounds = float3(-1, -1, -1); MaxBounds = float3(1, 1, 1); } + else if (ShapeConstant == CYLINDER) + { + MinBounds = InMinBounds; // radius, angle, height + MaxBounds = InMaxBounds; // radius, angle, height + } else if (ShapeConstant == ELLIPSOID) { MinBounds = InMinBounds; // longitude, sine(latitude), height @@ -486,6 +491,90 @@ struct ShapeUtility setShapeIntersections(Intersections, ListState, 0, result); } + /** + * Tests whether the input ray intersects the volume defined by two xy-planes at the specified z values. + */ + RayIntersections IntersectHeightBounds(in Ray R, in float2 HeightBounds, in bool IsConvex) + { + float zPosition = R.Origin.z; + float zDirection = R.Direction.z; + + float tmin = (HeightBounds.x - zPosition) / zDirection; + float tmax = (HeightBounds.y - zPosition) / zDirection; + + // Normals point outside the volume + float signFlip = IsConvex ? 1.0 : -1.0; + Intersection min = Utils.NewIntersection(tmin, float3(0.0, 0.0, -1.0 * signFlip)); + Intersection max = Utils.NewIntersection(tmax, float3(0.0, 0.0, 1.0 * signFlip)); + + bool isTopEntry = zDirection < 0.0; + if (isTopEntry) + { + return Utils.NewRayIntersections(max, min); + } + else + { + return Utils.NewRayIntersections(min, max); + } + } + + /** + * Tests whether the input ray intersects an infinite cylinder with the given radius. + */ + RayIntersections IntersectInfiniteCylinder(in Ray R, in float Radius, in bool IsConvex) + { + float3 o = R.Origin; + float3 d = R.Direction; + + float a = dot(d.xy, d.xy); + float b = dot(o.xy, d.xy); + float c = dot(o.xy, o.xy) - Radius * Radius; + float determinant = b * b - a * c; + + if (determinant < 0.0) + { + Intersection miss = Utils.NewIntersection(NO_HIT, normalize(R.Direction)); + return Utils.NewRayIntersections(miss, miss); + } + + determinant = sqrt(determinant); + float t1 = (-b - determinant) / a; + float t2 = (-b + determinant) / a; + float signFlip = IsConvex ? 1.0 : -1.0; + Intersection intersect1 = Utils.NewIntersection(t1, float3(normalize(o.xy + t1 * d.xy) * signFlip, 0.0)); + Intersection intersect2 = Utils.NewIntersection(t2, float3(normalize(o.xy + t2 * d.xy) * signFlip, 0.0)); + + return Utils.NewRayIntersections(intersect1, intersect2); + } + + /** + * Tests whether the given ray intersects a right cylindrical solid of the given radius and height bounds. + * The shape is assumed to be convex. + */ + RayIntersections IntersectBoundedCylinder(in Ray R, in float Radius, in float2 MinMaxHeight) + { + RayIntersections infiniteCylinderIntersection = IntersectInfiniteCylinder(R, Radius, true); + RayIntersections heightIntersection = IntersectHeightBounds(R, MinMaxHeight, true); + return Utils.ResolveIntersections(infiniteCylinderIntersection, heightIntersection); + } + + /** + * Tests whether the input ray (Unit Space) intersects the cylinder. Outputs the intersections in Unit Space. + */ + void IntersectCylinder(in Ray R, out float4 Intersections[INTERSECTIONS_LENGTH]) + { + ListState.Length = 2; + + RayIntersections OuterResult = IntersectBoundedCylinder(R, MaxBounds.x, float2(MinBounds.z, MaxBounds.z)); + setShapeIntersections(Intersections, ListState, 0, OuterResult); + + // In CesiumJS there's a check for the inner cylinder, because clipping planes / exaggeration + // can adjust the min radius. But it currently is not supported in Cesium for Unreal, so we + // exclude the math here for simplicity. + + // The same comment applies to the angle bounds of the cylinder. + } + /** * Tests whether the input ray intersects the ellipsoid at the given height, relative to original radii. */ @@ -1138,6 +1227,9 @@ struct ShapeUtility case BOX: IntersectBox(R, Intersections); break; + case CYLINDER: + IntersectCylinder(R, Intersections); + break; case ELLIPSOID: IntersectEllipsoid(R, Intersections); break; @@ -1224,6 +1316,35 @@ struct ShapeUtility return PositionUV; } + /** + * Converts the input position (vanilla UV Space) to its Shape UV Space relative to the + * cylinder geometry. Also outputs the Jacobian transpose for future use. + */ + float3 ConvertUVToShapeUVSpaceCylinder(in float3 PositionUV, out float3x3 JacobianT) + { + // First convert UV to "Unit Space derivative". + float3 unitPosition = UVToUnit(PositionUV); + + float radius = length(unitPosition.xy); // [0, 1] + float3 radial = normalize(float3(unitPosition.xy, 0.0)); + + // Shape space height is defined within [0, 1] + float height = PositionUV.z; // [0, 1] + float3 z = float3(0.0, 0.0, 1.0); + + float angle = atan2(unitPosition.y, unitPosition.x); + float3 east = normalize(float3(-unitPosition.y, unitPosition.x, 0.0)); + + JacobianT = float3x3(radial, z, east / length(unitPosition.xy)); + + // Then convert Unit Space to Shape UV Space. + angle = (angle + CZM_PI) / CZM_TWO_PI; + + // This currently does nothing for height / radius because the bounds of the cylinder are always the same. + // Once clipping / exaggeration are added, this must change. (See CesiumJS) + return float3(radius, angle, height); + } + /** * Given a specified point, gets the nearest point (XY) on the ellipse using a robust * iterative solution without trig functions, as well as the radius of curvature @@ -1343,6 +1464,8 @@ struct ShapeUtility { case BOX: return ConvertUVToShapeUVSpaceBox(UVPosition, JacobianT); + case CYLINDER: + return ConvertUVToShapeUVSpaceCylinder(UVPosition, JacobianT); case ELLIPSOID: return ConvertUVToShapeUVSpaceEllipsoid(UVPosition, JacobianT); default: @@ -1701,20 +1824,28 @@ struct VoxelDataTextures // Compute int coordinates of the voxel within the tile. float3 LocalUV = Sample.LocalUV; uint3 DataDimensions = GridDimensions + PaddingBefore + PaddingAfter; - + + float3 VoxelCoords = floor(LocalUV * float3(GridDimensions)); + // Account for padding + VoxelCoords = clamp(VoxelCoords + float3(PaddingBefore), 0, float3(DataDimensions - 1u)); + if (ShapeConstant == BOX) { // Since glTFs are y-up (and 3D Tiles is z-up), the data must be accessed to reflect the transforms // from a Y-up to Z-up frame of reference, plus the Cesium -> Unreal transform as well. - LocalUV = float3(LocalUV.x, clamp(1.0 - LocalUV.z, 0.0, 1.0), LocalUV.y); + VoxelCoords = float3(VoxelCoords.x, DataDimensions.y - VoxelCoords.z, VoxelCoords.y); + } + else if (ShapeConstant == CYLINDER) + { + // For cylinders, the start of the angular bounds has to be adjusted for full cylinders + // (i.e., the root tile only). + // TODO: This assumes that the root tile is a whole cylinder, but the root tile may be + // a partial cylinder. + float Angle = (Sample.Coords.w == 0) ? (VoxelCoords.y + DataDimensions.z / 2.0) % DataDimensions.z : VoxelCoords.y; + VoxelCoords = float3(VoxelCoords.x, VoxelCoords.z, Angle); } - - float3 VoxelCoords = floor(LocalUV * float3(GridDimensions)); - // Account for padding - VoxelCoords = clamp(VoxelCoords + float3(PaddingBefore), 0, float3(DataDimensions - 1u)); int3 Coords = TileCoords + VoxelCoords; - CustomShaderProperties Properties = (CustomShaderProperties) 0; %s @@ -1786,6 +1917,7 @@ DataTextures.TileCount = TileCount; switch (ShapeConstant) { case BOX: + case CYLINDER: DataTextures.GridDimensions = round(GridDimensions.xzy); DataTextures.PaddingBefore = round(PaddingBefore.xzy); DataTextures.PaddingAfter = round(PaddingAfter.xzy); diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index a1366167b..018443f71 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -131,6 +131,7 @@ void AutoFillVoxelClassDescription( FCesiumVoxelClassDescription& Description, const std::string& VoxelClassID, const Cesium3DTiles::Class& VoxelClass) { + Description.ID = VoxelClassID.c_str(); for (const auto& propertyIt : VoxelClass.properties) { auto pExistingProperty = Description.Properties.FindByPredicate( diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 6d73f1668..83153f07d 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -70,6 +70,9 @@ EVoxelGridShape getVoxelGridShape( if (std::get_if(&boundingVolume)) { return EVoxelGridShape::Box; } + if (std::get_if(&boundingVolume)) { + return EVoxelGridShape::Cylinder; + } if (std::get_if(&boundingVolume)) { return EVoxelGridShape::Ellipsoid; } @@ -112,6 +115,60 @@ void setVoxelBoxProperties( FVector4(0, 0, 0.02, 0)); } +void setVoxelCylinderProperties( + UCesiumVoxelRendererComponent* pVoxelComponent, + UMaterialInstanceDynamic* pVoxelMaterial, + const CesiumGeometry::BoundingCylinderRegion& cylinder) { + // Approximate the cylinder region as a box. + const CesiumGeometry::OrientedBoundingBox& box = + cylinder.toOrientedBoundingBox(); + + glm::dmat3 halfAxes = box.getHalfAxes(); + pVoxelComponent->HighPrecisionTransform = glm::dmat4( + glm::dvec4(halfAxes[0], 0) * 0.02, + glm::dvec4(halfAxes[1], 0) * 0.02, + glm::dvec4(halfAxes[2], 0) * 0.02, + glm::dvec4(box.getCenter(), 1)); + + // For now, only the height bounds and maximum radius are used. + // The angle will be relevant when clipping. + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape Min Bounds"), + EMaterialParameterAssociation::LayerParameter, + 0), + FVector(0, -CesiumUtility::Math::OnePi, -1)); + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape Max Bounds"), + EMaterialParameterAssociation::LayerParameter, + 0), + FVector(1, CesiumUtility::Math::OnePi, 1)); + + // The transform and scale of the cylinder are handled in the component's + // transform, so there is no need to duplicate it here. Instead, this + // transform is configured to scale the engine-provided Cube ([-50, 50]) to + // unit space ([-1, 1]). + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape TransformToUnit Row 0"), + EMaterialParameterAssociation::LayerParameter, + 0), + FVector4(0.02, 0, 0, 0)); + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape TransformToUnit Row 1"), + EMaterialParameterAssociation::LayerParameter, + 0), + FVector4(0, 0.02, 0, 0)); + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape TransformToUnit Row 2"), + EMaterialParameterAssociation::LayerParameter, + 0), + FVector4(0, 0, 0.02, 0)); +} + /** * @brief Describes the quality of a radian value relative to the axis it is * defined in. This determines the math for the ray-intersection tested against @@ -529,6 +586,11 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( std::get_if(&boundingVolume); assert(pBox != nullptr); setVoxelBoxProperties(pVoxelComponent, pVoxelMaterial, *pBox); + } else if (shape == EVoxelGridShape::Cylinder) { + const CesiumGeometry::BoundingCylinderRegion* pCylinder = + std::get_if(&boundingVolume); + assert(pCylinder != nullptr); + setVoxelCylinderProperties(pVoxelComponent, pVoxelMaterial, *pCylinder); } else if (shape == EVoxelGridShape::Ellipsoid) { const CesiumGeospatial::BoundingRegion* pRegion = std::get_if(&boundingVolume); From d7666c5f868eb93177ef3ae6f2f2ec49912fec55 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 14 Jul 2025 11:15:59 -0400 Subject: [PATCH 02/13] Update shaders for cylinder --- Shaders/Private/CesiumCylinder.usf | 138 ++++++++++++++++++++++++ Shaders/Private/CesiumShape.usf | 14 ++- Shaders/Private/CesiumVoxelTemplate.usf | 20 +++- 3 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 Shaders/Private/CesiumCylinder.usf diff --git a/Shaders/Private/CesiumCylinder.usf b/Shaders/Private/CesiumCylinder.usf new file mode 100644 index 000000000..d0a766dc9 --- /dev/null +++ b/Shaders/Private/CesiumCylinder.usf @@ -0,0 +1,138 @@ +#pragma once + +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumCylinder.usf: An implicit cylinder that may be intersected by a ray. +=============================================================================*/ + +#include "CesiumRayIntersectionList.usf" + +struct Cylinder +{ + float3 MinBounds; + float3 MaxBounds; + + /** + * Tests whether the input ray intersects the volume defined by two xy-planes at the specified z values. + */ + RayIntersections IntersectHeightBounds(in Ray R, in float2 HeightBounds, in bool IsConvex) + { + float zPosition = R.Origin.z; + float zDirection = R.Direction.z; + + float tmin = (HeightBounds.x - zPosition) / zDirection; + float tmax = (HeightBounds.y - zPosition) / zDirection; + + // Normals point outside the volume + float signFlip = IsConvex ? 1.0 : -1.0; + Intersection min = NewIntersection(tmin, float3(0.0, 0.0, -1.0 * signFlip)); + Intersection max = NewIntersection(tmax, float3(0.0, 0.0, 1.0 * signFlip)); + + bool isTopEntry = zDirection < 0.0; + if (isTopEntry) + { + return NewRayIntersections(max, min); + } + else + { + return NewRayIntersections(min, max); + } + } + + /** + * Tests whether the input ray intersects an infinite cylinder with the given radius. + */ + RayIntersections IntersectInfiniteCylinder(in Ray R, in float Radius, in bool IsConvex) + { + float3 o = R.Origin; + float3 d = R.Direction; + + float a = dot(d.xy, d.xy); + float b = dot(o.xy, d.xy); + float c = dot(o.xy, o.xy) - Radius * Radius; + float determinant = b * b - a * c; + + if (determinant < 0.0) + { + Intersection miss = NewIntersection(NO_HIT, normalize(R.Direction)); + return NewRayIntersections(miss, miss); + } + + determinant = sqrt(determinant); + float t1 = (-b - determinant) / a; + float t2 = (-b + determinant) / a; + float signFlip = IsConvex ? 1.0 : -1.0; + Intersection intersect1 = NewIntersection(t1, float3(normalize(o.xy + t1 * d.xy) * signFlip, 0.0)); + Intersection intersect2 = NewIntersection(t2, float3(normalize(o.xy + t2 * d.xy) * signFlip, 0.0)); + + return NewRayIntersections(intersect1, intersect2); + } + + /** + * Tests whether the given ray intersects a right cylindrical solid of the given radius and height bounds. + * The shape is assumed to be convex. + */ + RayIntersections IntersectBoundedCylinder(in Ray R, in float Radius, in float2 MinMaxHeight) + { + RayIntersections infiniteCylinderIntersection = IntersectInfiniteCylinder(R, Radius, true); + RayIntersections heightIntersection = IntersectHeightBounds(R, MinMaxHeight, true); + return ResolveIntersections(infiniteCylinderIntersection, heightIntersection); + } + + /** + * Tests whether the input ray (Unit Space) intersects the cylinder. Outputs the intersections in Unit Space. + */ + void IntersectCylinder(in Ray R, out float4 Intersections[INTERSECTIONS_LENGTH], inout IntersectionListState ListState) + { + ListState.Length = 2; + + RayIntersections OuterResult = IntersectBoundedCylinder(R, MaxBounds.x, float2(MinBounds.z, MaxBounds.z)); + setShapeIntersections(Intersections, ListState, 0, OuterResult); + + // In CesiumJS there's a check for the inner cylinder, because clipping planes / exaggeration + // can adjust the min radius. But it currently is not supported in Cesium for Unreal, so we + // exclude the math here for simplicity. + + // The same comment applies to the angle bounds of the cylinder. + } + + /** + * Scales the input UV coordinates from [0, 1] to their values in UV Shape Space. + */ + float3 ScaleUVToShapeUVSpace(in float3 UV) + { + // For CYLINDER: Once clipping / exaggeration is supported, an offset + scale + // should be applied to its radius / height / angle. (See CesiumJS) + return UV; + } + + /** + * Converts the input position (vanilla UV Space) to its Shape UV Space relative to the + * ellipsoid geometry. Also outputs the Jacobian transpose for future use. + */ + float3 ConvertUVToShapeUVSpace(in float3 PositionUV, out float3x3 JacobianT) + { + // First convert UV to "Unit Space derivative". + float3 unitPosition = UVToUnit(PositionUV); + + float radius = length(unitPosition.xy); // [0, 1] + float3 radial = normalize(float3(unitPosition.xy, 0.0)); + + // Shape space height is defined within [0, 1] + float height = PositionUV.z; // [0, 1] + float3 z = float3(0.0, 0.0, 1.0); + + float angle = atan2(unitPosition.y, unitPosition.x); + float3 east = normalize(float3(-unitPosition.y, unitPosition.x, 0.0)); + + JacobianT = float3x3(radial, z, east / length(unitPosition.xy)); + + // Then convert Unit Space to Shape UV Space. + angle = (angle + CZM_PI) / CZM_TWO_PI; + + // This currently does nothing for height / radius because the bounds of the cylinder are always the same. + // Once clipping / exaggeration are added, this must change. (See CesiumJS) + return float3(radius, angle, height); + } +}; diff --git a/Shaders/Private/CesiumShape.usf b/Shaders/Private/CesiumShape.usf index 49998e2ab..3a3d27b73 100644 --- a/Shaders/Private/CesiumShape.usf +++ b/Shaders/Private/CesiumShape.usf @@ -8,12 +8,14 @@ #include "CesiumShapeConstants.usf" #include "CesiumBox.usf" +#include "CesiumCylinder.usf" #include "CesiumEllipsoidRegion.usf" struct Shape { int ShapeConstant; Box BoxShape; + Cylinder CylinderShape; EllipsoidRegion RegionShape; IntersectionListState ListState; @@ -28,10 +30,13 @@ struct Shape switch (ShapeConstant) { case BOX: - // Initialize with default unit box bounds. + // Initialize with default unit box bounds. BoxShape.MinBounds = -1; BoxShape.MaxBounds = 1; break; + case CYLINDER: + CylinderShape.MinBounds = InMinBounds; // radius, angle, height + CylinderShape.MaxBounds = InMaxBounds; // radius, angle, height case ELLIPSOID: RegionShape.Initialize(InMinBounds, InMaxBounds, PackedData0, PackedData1, PackedData2, PackedData3, PackedData4, PackedData5); break; @@ -49,6 +54,9 @@ struct Shape case BOX: BoxShape.Intersect(R, Intersections, ListState); break; + case CYLINDER: + CylinderShape.IntersectCylinder(R, Intersections, ListState); + break; case ELLIPSOID: RegionShape.Intersect(R, Intersections, ListState); break; @@ -97,6 +105,8 @@ struct Shape [branch] switch (ShapeConstant) { + case CYLINDER: + return CylinderShape.ScaleUVToShapeUVSpace(UV); case ELLIPSOID: return RegionShape.ScaleUVToShapeUVSpace(UV); case BOX: @@ -115,6 +125,8 @@ struct Shape { case BOX: return BoxShape.ConvertUVToShapeUVSpace(UVPosition, JacobianT); + case CYLINDER: + return CylinderShape.ConvertUVToShapeUVSpace(UVPosition, JacobianT); case ELLIPSOID: return RegionShape.ConvertUVToShapeUVSpace(UVPosition, JacobianT); default: diff --git a/Shaders/Private/CesiumVoxelTemplate.usf b/Shaders/Private/CesiumVoxelTemplate.usf index 1353202d3..f535f3458 100644 --- a/Shaders/Private/CesiumVoxelTemplate.usf +++ b/Shaders/Private/CesiumVoxelTemplate.usf @@ -68,16 +68,25 @@ struct VoxelMegatextures float3 LocalUV = Sample.LocalUV; uint3 DataDimensions = GridDimensions + PaddingBefore + PaddingAfter; + float3 VoxelCoords = floor(LocalUV * float3(GridDimensions)); + // Account for padding + VoxelCoords = clamp(VoxelCoords + float3(PaddingBefore), 0, float3(DataDimensions - 1u)); + if (ShapeConstant == BOX) { // Since glTFs are y-up (and 3D Tiles is z-up), the data must be accessed to reflect the transforms // from a Y-up to Z-up frame of reference, plus the Cesium -> Unreal transform as well. - LocalUV = float3(LocalUV.x, clamp(1.0 - LocalUV.z, 0.0, 1.0), LocalUV.y); + VoxelCoords = float3(VoxelCoords.x, DataDimensions.y - VoxelCoords.z, VoxelCoords.y); } - - float3 VoxelCoords = floor(LocalUV * float3(GridDimensions)); - // Account for padding - VoxelCoords = clamp(VoxelCoords + float3(PaddingBefore), 0, float3(DataDimensions - 1u)); + else if (ShapeConstant == CYLINDER) + { + // For cylinders, the start of the angular bounds has to be adjusted for full cylinders + // (i.e., the root tile only). + // TODO: This assumes that the root tile is a whole cylinder, but the root tile may be + // a partial cylinder. + float Angle = (Sample.Coords.w == 0) ? (VoxelCoords.y + DataDimensions.z / 2.0) % DataDimensions.z : VoxelCoords.y; + VoxelCoords = float3(VoxelCoords.x, VoxelCoords.z, Angle); + } int3 Coords = TileCoords + VoxelCoords; @@ -162,6 +171,7 @@ DataTextures.TileCount = TileCount; // Account for y-up -> z-up conventions for certain shapes. switch (ShapeConstant) { case BOX: + case CYLINDER: DataTextures.GridDimensions = round(GridDimensions.xzy); DataTextures.PaddingBefore = round(PaddingBefore.xzy); DataTextures.PaddingAfter = round(PaddingAfter.xzy); From f25b3ef780a746e9f734d990366ec53a73214b1c Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 14 Jul 2025 15:58:26 -0400 Subject: [PATCH 03/13] WIP partial cylinder support [skip ci] --- Shaders/Private/CesiumCylinder.usf | 146 +++++++++++-- Shaders/Private/CesiumEllipsoidRegion.usf | 9 +- Shaders/Private/CesiumLongitudeWedge.usf | 147 +++++++++++++ .../Private/CesiumVoxelRendererComponent.cpp | 193 +++++++++++++++--- 4 files changed, 445 insertions(+), 50 deletions(-) create mode 100644 Shaders/Private/CesiumLongitudeWedge.usf diff --git a/Shaders/Private/CesiumCylinder.usf b/Shaders/Private/CesiumCylinder.usf index d0a766dc9..c1400ce44 100644 --- a/Shaders/Private/CesiumCylinder.usf +++ b/Shaders/Private/CesiumCylinder.usf @@ -7,12 +7,47 @@ =============================================================================*/ #include "CesiumRayIntersectionList.usf" +#include "CesiumLongitudeWedge.usf" struct Cylinder { float3 MinBounds; float3 MaxBounds; + float RadiusUVScale; + float RadiusUVOffset; + + float3 AngleUVExtents; // X = min, Y = max, Z = midpoint + float AngleUVScale; + float AngleUVOffset; + + bool RadiusHasMinimumBound; + bool RadiusIsFlat; + + uint AngleRangeFlag; + bool AngleMinHasDiscontinuity; + bool AngleMaxHasDiscontinuity; + bool AngleIsReversed; + + void Initialize(float3 InMinBounds, float3 InMaxBounds, float4 PackedData0, float4 PackedData1, float4 PackedData2) + { + MinBounds = InMinBounds; // radius, angle, height + MaxBounds = InMaxBounds; // radius, angle, height + + // Flags are packed in CesiumVoxelRendererComponent.cpp + RadiusHasMinimumBound = bool(PackedData0.x); + + AngleRangeFlag = round(PackedData1.x); + AngleMinHasDiscontinuity = bool(PackedData1.y); + AngleMaxHasDiscontinuity = bool(PackedData1.z); + AngleIsReversed = bool(PackedData1.w); + + RadiusUVScale = PackedData2.x; + RadiusUVOffset = PackedData2.y; + AngleUVScale = PackedData2.z; + AngleUVOffset = PackedData2.w; + } + /** * Tests whether the input ray intersects the volume defined by two xy-planes at the specified z values. */ @@ -29,9 +64,9 @@ struct Cylinder Intersection min = NewIntersection(tmin, float3(0.0, 0.0, -1.0 * signFlip)); Intersection max = NewIntersection(tmax, float3(0.0, 0.0, 1.0 * signFlip)); - bool isTopEntry = zDirection < 0.0; - if (isTopEntry) + if (zDirection < 0.0) { + // Ray entered from the top plane down. return NewRayIntersections(max, min); } else @@ -90,11 +125,56 @@ struct Cylinder RayIntersections OuterResult = IntersectBoundedCylinder(R, MaxBounds.x, float2(MinBounds.z, MaxBounds.z)); setShapeIntersections(Intersections, ListState, 0, OuterResult); - // In CesiumJS there's a check for the inner cylinder, because clipping planes / exaggeration - // can adjust the min radius. But it currently is not supported in Cesium for Unreal, so we - // exclude the math here for simplicity. + if (OuterResult.Entry.t == NO_HIT) + { + return; + } - // The same comment applies to the angle bounds of the cylinder. + if (RadiusIsFlat) + { + // When the cylinder is perfectly thin it's necessary to sandwich the + // inner cylinder intersection inside the outer cylinder intersection. + + // Without this special case, + // [outerMin, outerMax, innerMin, innerMax] will bubble sort to + // [outerMin, innerMin, outerMax, innerMax] which will cause the back + // side of the cylinder to be invisible because it will think the ray + // is still inside the inner (negative) cylinder after exiting the + // outer (positive) cylinder. + + // With this special case, + // [outerMin, innerMin, innerMax, outerMax] will bubble sort to + // [outerMin, innerMin, innerMax, outerMax] which will work correctly. + + // Note: If Sort() changes its sorting function + // from bubble sort to something else, this code may need to change. + RayIntersections InnerResult = IntersectInfiniteCylinder(R, 1.0, false); + setSurfaceIntersection(Intersections, ListState, 0, OuterResult.Entry, true, true); // positive, entering + setSurfaceIntersection(Intersections, ListState, 1, InnerResult.Entry, false, true); // negative, entering + setSurfaceIntersection(Intersections, ListState, 2, InnerResult.Exit, false, false); // negative, exiting + setSurfaceIntersection(Intersections, ListState, 3, OuterResult.Exit, true, false); // positive, exiting + } + else if (RadiusHasMinimumBound) + { + RayIntersections InnerResult = IntersectInfiniteCylinder(R, MinBounds.x, false); + setShapeIntersection(Intersections, ListState, 1, InnerResult); + } + + // TODO +#if defined(CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_UNDER_HALF) + RayShapeIntersection wedgeIntersect = intersectRegularWedge(ray, u_cylinderRenderAngleMinMax); + setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_ANGLE, wedgeIntersect); +#elif defined(CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_OVER_HALF) + RayShapeIntersection wedgeIntersects[2]; + intersectFlippedWedge(ray, u_cylinderRenderAngleMinMax, wedgeIntersects); + setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_ANGLE + 0, wedgeIntersects[0]); + setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_ANGLE + 1, wedgeIntersects[1]); +#elif defined(CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_EQUAL_ZERO) + RayShapeIntersection wedgeIntersects[2]; + intersectHalfPlane(ray, u_cylinderRenderAngleMinMax.x, wedgeIntersects); + setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_ANGLE + 0, wedgeIntersects[0]); + setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_ANGLE + 1, wedgeIntersects[1]); +#endif } /** @@ -102,9 +182,20 @@ struct Cylinder */ float3 ScaleUVToShapeUVSpace(in float3 UV) { - // For CYLINDER: Once clipping / exaggeration is supported, an offset + scale - // should be applied to its radius / height / angle. (See CesiumJS) - return UV; + float radius = UV.x; + if (RadiusHasMinimumBound) + { + radius /= RadiusUVScale; + } + + // Convert from [0, 1] to radians [-pi, pi] + float angle = UV.y * CZM_TWO_PI; + if (AngleRangeFlag > 0) + { + angle /= AngleUVScale; + } + + return float3(radius, angle, UV.z); } /** @@ -129,10 +220,39 @@ struct Cylinder JacobianT = float3x3(radial, z, east / length(unitPosition.xy)); // Then convert Unit Space to Shape UV Space. - angle = (angle + CZM_PI) / CZM_TWO_PI; + // Radius: shift and scale to [0, 1] + float radiusUV = radius; + if (RadiusHasMinimumBound) + { + radiusUV = radiusUV * RadiusUVScale + RadiusUVOffset; + } + + // Angle: shift and scale to [0, 1] + float angleUV = (angle + CZM_PI) / CZM_TWO_PI; + + // Correct the angle when max < min + // Technically this should compare against min angle, but it has precision problems so compare against the middle of empty space. + if (AngleIsReversed) + { + angleUV += float(angleUV < AngleUVExtents.z); + } + + // Avoid flickering from reading voxels from both sides of the -pi/+pi discontinuity. + if (AngleMinHasDiscontinuity) + { + angleUV = angleUV > AngleUVExtents.z ? AngleUVExtents.x : angleUV; + } + + if (AngleMaxHasDiscontinuity) + { + angleUV = angleUV < AngleUVExtents.z ? AngleUVExtents.y : angleUV; + } + + if (AngleRangeFlag > 0) + { + angleUV = angleUV * AngleUVScale + AngleUVOffset; + } - // This currently does nothing for height / radius because the bounds of the cylinder are always the same. - // Once clipping / exaggeration are added, this must change. (See CesiumJS) - return float3(radius, angle, height); + return float3(radiusUV, angleUV, height); } }; diff --git a/Shaders/Private/CesiumEllipsoidRegion.usf b/Shaders/Private/CesiumEllipsoidRegion.usf index 56b08b6c1..6e4a5f38d 100644 --- a/Shaders/Private/CesiumEllipsoidRegion.usf +++ b/Shaders/Private/CesiumEllipsoidRegion.usf @@ -7,6 +7,7 @@ =============================================================================*/ #include "CesiumRayIntersectionList.usf" +#include "CesiumLongitudeWedge.usf" struct EllipsoidRegion { @@ -460,12 +461,6 @@ struct EllipsoidRegion return NewRayIntersections(Multiply(farSide, -1), intersect); } } - - -#define ANGLE_EQUAL_ZERO 1 -#define ANGLE_UNDER_HALF 2 -#define ANGLE_HALF 3 -#define ANGLE_OVER_HALF 4 /** * Tests whether the input ray (Unit Space) intersects the ellipsoid region. Outputs the intersections in Unit Space. @@ -760,7 +755,7 @@ struct EllipsoidRegion } - /** + /** * Converts the input position (vanilla UV Space) to its Shape UV Space relative to the * ellipsoid geometry. Also outputs the Jacobian transpose for future use. */ diff --git a/Shaders/Private/CesiumLongitudeWedge.usf b/Shaders/Private/CesiumLongitudeWedge.usf new file mode 100644 index 000000000..9a82bc279 --- /dev/null +++ b/Shaders/Private/CesiumLongitudeWedge.usf @@ -0,0 +1,147 @@ +#pragma once + +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumLongitudeWedge.usf: An implicit longitude range (i.e., an angular wedge) that may be intersected by a ray. +=============================================================================*/ + +#include "CesiumRayIntersection.usf" + +#define ANGLE_EQUAL_ZERO 1 +#define ANGLE_UNDER_HALF 2 +#define ANGLE_HALF 3 +#define ANGLE_OVER_HALF 4 + +/** +* Intersects the plane at the specified longitude angle. +*/ +Intersection IntersectLongitudePlane(in Ray R, in float Angle, in bool PositiveNormal) +{ + float normalSign = PositiveNormal ? 1.0 : -1.0; + float2 planeNormal = normalSign * float2(-sin(Angle), cos(Angle)); + + float approachRate = dot(R.Direction.xy, planeNormal); + float distance = -dot(R.Origin.xy, planeNormal); + + float t = (approachRate == 0.0) + ? NO_HIT + : distance / approachRate; + + return NewIntersection(t, float3(planeNormal, 0)); +} + +/** +* Intersects the space on one side of the specified longitude angle. +*/ +RayIntersections IntersectHalfSpace(in Ray R, in float Angle, in bool PositiveNormal) +{ + Intersection intersection = IntersectLongitudePlane(R, Angle, PositiveNormal); + Intersection farSide = NewIntersection(INF_HIT, normalize(R.Direction)); + + bool hitFront = (intersection.t > 0.0) == (dot(R.Origin.xy, intersection.Normal.xy) > 0.0); + if (!hitFront) + { + return NewRayIntersections(intersection, farSide); + } + else + { + return NewRayIntersections(Multiply(farSide, -1), intersection); + } +} + +/** +* Intersects a "flipped" wedge formed by the negative space of the specified angle +* bounds and returns up to four intersections. The "flipped" wedge is the union of +* two half-spaces defined at the given angles and represents a *negative* volume +* of over > 180 degrees. +*/ +void IntersectFlippedWedge(in Ray R, in float2 AngleBounds, out RayIntersections FirstIntersection, out RayIntersections SecondIntersection) +{ + FirstIntersection = IntersectHalfSpace(R, AngleBounds.x, false); + SecondIntersection = IntersectHalfSpace(R, AngleBounds.y, true); +} + +/** +* Intersects the wedge formed by the negative space of the min/max longitude, where +* maxAngle > minAngle + pi. The wedge is represented by two planes at such angles. +* There is an opposite "shadow wedge", i.e. the wedge formed at the *opposite* side +* of the planes' intersection, that must be specially handled. +*/ +RayIntersections IntersectRegularWedge(in Ray R, in float2 AngleBounds) +{ + // Normals will point toward the "outside" (into the negative space) + Intersection intersect1 = IntersectLongitudePlane(R, AngleBounds.x, false); + Intersection intersect2 = IntersectLongitudePlane(R, AngleBounds.y, true); + + // Note: the intersections could be in the "shadow" wedge, beyond the tip of + // the actual wedge. + Intersection first = intersect1; + Intersection last = intersect2; + if (first.t > last.t) + { + first = intersect2; + last = intersect1; + } + + bool firstIntersectionAheadOfRay = first.t >= 0.0; + bool startedInsideFirst = dot(R.Origin.xy, first.Normal.xy) < 0.0; + bool isExitingFromInside = firstIntersectionAheadOfRay == startedInsideFirst; + bool lastIntersectionAheadOfRay = last.t > 0.0; + bool startedOutsideLast = dot(R.Origin.xy, last.Normal.xy) >= 0.0; + bool isEnteringFromOutside = lastIntersectionAheadOfRay == startedOutsideLast; + + Intersection farSide = NewIntersection(INF_HIT, normalize(R.Direction)); + Intersection miss = NewIntersection(NO_HIT, normalize(R.Direction)); + + if (isExitingFromInside && isEnteringFromOutside) + { + // Ray crosses both faces of negative wedge, exiting then entering the positive shape + return NewRayIntersections(first, last); + } + else if (!isExitingFromInside && isEnteringFromOutside) + { + // Ray starts inside wedge. last is in shadow wedge, and first is actually the entry + return NewRayIntersections(Multiply(farSide, -1), first); + } + else if (isExitingFromInside && !isEnteringFromOutside) + { + // First intersection was in the shadow wedge, so last is actually the exit + return NewRayIntersections(last, farSide); + } + else + { // !exitFromInside && !enterFromOutside + // Both intersections were in the shadow wedge + return NewRayIntersections(miss, miss); + } +} + +bool HitsPositiveHalfPlane(in Ray R, in Intersection InputIntersection, in bool positiveNormal) +{ + float normalSign = positiveNormal ? 1.0 : -1.0; + float2 planeDirection = float2(InputIntersection.Normal.y, -InputIntersection.Normal.x) * normalSign; + float2 hit = R.Origin.xy + InputIntersection.t * R.Direction.xy; + return dot(hit, planeDirection) > 0.0; +} + +void IntersectHalfPlane(in Ray R, in float Angle, out RayIntersections FirstIntersection, out RayIntersections SecondIntersection) +{ + Intersection intersection = IntersectLongitudePlane(R, Angle, true); + Intersection farSide = NewIntersection(INF_HIT, normalize(R.Direction)); + + if (HitsPositiveHalfPlane(R, intersection, true)) + { + FirstIntersection.Entry = Multiply(farSide, -1); + FirstIntersection.Exit = NewIntersection(intersection.t, float3(-1.0 * intersection.Normal.xy, 0.0)); + SecondIntersection.Entry = intersection; + SecondIntersection.Exit = farSide; + } + else + { + Intersection miss = NewIntersection(NO_HIT, normalize(R.Direction)); + FirstIntersection.Entry = Multiply(farSide, -1); + FirstIntersection.Exit = farSide; + SecondIntersection.Entry = miss; + SecondIntersection.Exit = miss; + } +} diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index e596e8ec3..a8cae7619 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -132,6 +132,40 @@ void setVoxelBoxProperties( FVector4(0, 0, 0.02, 0)); } +/** + * @brief Describes the quality of a radian value relative to the axis it is + * defined in. This determines the math for the ray-intersection tested against + * that value in the voxel shader. + */ +enum AngleDescription : int8 { + None = 0, + Zero = 1, + UnderHalf = 2, + Half = 3, + OverHalf = 4 +}; + +AngleDescription interpretCylinderAngle(double value) { + const double angleEpsilon = CesiumUtility::Math::Epsilon10; + + if (value > angleEpsilon && + value < CesiumUtility::Math::TwoPi - angleEpsilon) { + // angle range > PI + return AngleDescription::OverHalf; + } + if (value < CesiumUtility::Math::OnePi - angleEpsilon) { + // angle range < PI + return AngleDescription::UnderHalf; + } + if (value >= CesiumUtility::Math::OnePi - angleEpsilon && + value <= CesiumUtility::Math::OnePi + angleEpsilon) { + // angle range ~= PI + return AngleDescription::Half; + } + + return AngleDescription::None; +} + void setVoxelCylinderProperties( UCesiumVoxelRendererComponent* pVoxelComponent, UMaterialInstanceDynamic* pVoxelMaterial, @@ -147,20 +181,132 @@ void setVoxelCylinderProperties( glm::dvec4(halfAxes[2], 0) * 0.02, glm::dvec4(box.getCenter(), 1)); - // For now, only the height bounds and maximum radius are used. - // The angle will be relevant when clipping. + // The default bounds define the minimum and maximum extents for the shape's + // actual bounds, in the order of (radius, angle, height). + const FVector defaultMinimumBounds(0, -CesiumUtility::Math::OnePi, -1); + const FVector defaultMaximumBounds(1, CesiumUtility::Math::OnePi, 1); + + const glm::dvec2& radialBounds = cylinder.getRadialBounds(); + const glm::dvec2& angularBounds = cylinder.getAngularBounds(); + + double normalizedMinimumRadius = radialBounds.x / radialBounds.y; + double radiusUVScale = 1; + double radiusUVOffset = 0; + FIntPoint radiusFlags(0); + + // Radius + { + double radiusRange = radialBounds.y - radialBounds.x; + bool hasNonzeroMinimumRadius = normalizedMinimumRadius > 0.0; + bool hasFlatRadius = radialBounds.x == radialBounds.y; + + if (hasNonzeroMinimumRadius && radiusRange > 0.0) { + radiusUVScale = 1.0 / radiusRange; + radiusUVOffset = -radialBounds.x / radiusRange; + } + + radiusFlags.X = hasNonzeroMinimumRadius; + radiusFlags.Y = hasFlatRadius; + } + + // Defines the extents of the longitude in UV space. In other words, this + // expresses the minimum, maximum, and midpoint values of the longitude range + // in UV space. + FVector angleUVExtents = FVector::Zero(); + + double angleUVScale = 1.0; + double angleUVOffset = 0.0; + FIntVector4 angleFlags(0); + // Angle + { + const double defaultRange = CesiumUtility::Math::TwoPi; + bool isAngleReversed = angularBounds.y < angularBounds.x; + double angleRange = + angularBounds.y - angularBounds.x + isAngleReversed * defaultRange; + AngleDescription angleRangeIndicator = interpretCylinderAngle(angleRange); + + // Refers to the discontinuity at angle -pi / pi. + const double discontinuityEpsilon = + CesiumUtility::Math::Epsilon3; // 0.001 radians = 0.05729578 degrees + bool angleMinimumHasDiscontinuity = CesiumUtility::Math::equalsEpsilon( + angularBounds.x, + -CesiumUtility::Math::OnePi, + discontinuityEpsilon); + bool angleMaximumHasDiscontinuity = CesiumUtility::Math::equalsEpsilon( + angularBounds.y, + CesiumUtility::Math::OnePi, + discontinuityEpsilon); + + angleFlags.X = angleRangeIndicator; + angleFlags.Y = angleMinimumHasDiscontinuity; + angleFlags.Z = angleMaximumHasDiscontinuity; + angleFlags.W = isAngleReversed; + + // Compute the extents of the angle range in UV Shape Space. + double minimumAngleUV = + (angularBounds.x - defaultMinimumBounds.Y) / defaultRange; + double maximumAngleUV = + (angularBounds.y - defaultMinimumBounds.Y) / defaultRange; + // Given an angle range, represents the actual value where "0" would be + // in UV coordinates. + double angleRangeUVZero = 1.0 - angleRange / defaultRange; + // TODO: document this + double angleRangeUVZeroMid = + glm::fract(maximumAngleUV + 0.5 * angleRangeUVZero); + + angleUVExtents = + FVector(minimumAngleUV, maximumAngleUV, angleRangeUVZeroMid); + + const double angleEpsilon = CesiumUtility::Math::Epsilon10; + if (angleRange > angleEpsilon) { + angleUVScale = defaultRange / angleRange; + angleUVOffset = -(angularBounds.x - defaultMinimumBounds.Y) / angleRange; + } + } + + // Shape Min Bounds = Cylinder Min (xyz) + // X = radius (normalized), Y = angle, Z = height (unused) pVoxelMaterial->SetVectorParameterValueByInfo( FMaterialParameterInfo( UTF8_TO_TCHAR("Shape Min Bounds"), EMaterialParameterAssociation::LayerParameter, 0), - FVector(0, -CesiumUtility::Math::OnePi, -1)); + FVector(normalizedMinimumRadius, angularBounds.x, -1)); + + // Shape Max Bounds = Cylinder Max (xyz) + // X = radius (normalized), Y = angle, Z = height (unused) pVoxelMaterial->SetVectorParameterValueByInfo( FMaterialParameterInfo( UTF8_TO_TCHAR("Shape Max Bounds"), EMaterialParameterAssociation::LayerParameter, 0), - FVector(1, CesiumUtility::Math::OnePi, 1)); + FVector(1.0, angularBounds.y, 1)); + + // Data is packed across multiple vec4s to conserve space. + // 0 = Radius Range Flags (xy) + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape Packed Data 0"), + EMaterialParameterAssociation::LayerParameter, + 0), + FVector4(radiusFlags.X, radiusFlags.Y)); + + // 1 = Angle Range Flags (xyzw) + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape Packed Data 1"), + EMaterialParameterAssociation::LayerParameter, + 0), + FVector4(angleFlags)); + + // 2 = UV -> Shape UV Transforms (scale / offset) + // Radius (xy), Angle (zw) + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape Packed Data 2"), + EMaterialParameterAssociation::LayerParameter, + 0), + FVector4(radiusUVScale, radiusUVOffset, angleUVScale, angleUVOffset)); // The transform and scale of the cylinder are handled in the component's // transform, so there is no need to duplicate it here. Instead, this @@ -186,41 +332,28 @@ void setVoxelCylinderProperties( FVector4(0, 0, 0.02, 0)); } -/** - * @brief Describes the quality of a radian value relative to the axis it is - * defined in. This determines the math for the ray-intersection tested against - * that value in the voxel shader. - */ -enum CartographicAngleDescription : int8 { - None = 0, - Zero = 1, - UnderHalf = 2, - Half = 3, - OverHalf = 4 -}; - -CartographicAngleDescription interpretLongitudeRange(double value) { +AngleDescription interpretLongitudeRange(double value) { const double longitudeEpsilon = CesiumUtility::Math::Epsilon10; if (value >= CesiumUtility::Math::OnePi - longitudeEpsilon && value < CesiumUtility::Math::TwoPi - longitudeEpsilon) { // longitude range > PI - return CartographicAngleDescription::OverHalf; + return AngleDescription::OverHalf; } if (value > longitudeEpsilon && value < CesiumUtility::Math::OnePi - longitudeEpsilon) { // longitude range < PI - return CartographicAngleDescription::UnderHalf; + return AngleDescription::UnderHalf; } if (value < longitudeEpsilon) { // longitude range ~= 0 - return CartographicAngleDescription::Zero; + return AngleDescription::Zero; } - return CartographicAngleDescription::None; + return AngleDescription::None; } -CartographicAngleDescription interpretLatitudeValue(double value) { +AngleDescription interpretLatitudeValue(double value) { const double latitudeEpsilon = CesiumUtility::Math::Epsilon10; const double zeroLatitudeEpsilon = CesiumUtility::Math::Epsilon3; // 0.001 radians = 0.05729578 degrees @@ -228,18 +361,18 @@ CartographicAngleDescription interpretLatitudeValue(double value) { if (value > -CesiumUtility::Math::OnePi + latitudeEpsilon && value < -zeroLatitudeEpsilon) { // latitude between (-PI, 0) - return CartographicAngleDescription::UnderHalf; + return AngleDescription::UnderHalf; } if (value >= -zeroLatitudeEpsilon && value <= +zeroLatitudeEpsilon) { // latitude ~= 0 - return CartographicAngleDescription::Half; + return AngleDescription::Half; } if (value > zeroLatitudeEpsilon) { // latitude between (0, PI) - return CartographicAngleDescription::OverHalf; + return AngleDescription::OverHalf; } - return CartographicAngleDescription::None; + return AngleDescription::None; } FVector getEllipsoidRadii(const ACesiumGeoreference* pGeoreference) { @@ -321,7 +454,7 @@ void setVoxelEllipsoidProperties( CesiumUtility::Math::TwoPi, discontinuityEpsilon); - CartographicAngleDescription longitudeRangeIndicator = + AngleDescription longitudeRangeIndicator = interpretLongitudeRange(longitudeRange); longitudeFlags.X = longitudeRangeIndicator; @@ -355,9 +488,9 @@ void setVoxelEllipsoidProperties( } // Latitude - CartographicAngleDescription latitudeMinValueFlag = + AngleDescription latitudeMinValueFlag = interpretLatitudeValue(minimumLatitude); - CartographicAngleDescription latitudeMaxValueFlag = + AngleDescription latitudeMaxValueFlag = interpretLatitudeValue(maximumLatitude); double latitudeUVScale = 1; double latitudeUVOffset = 0; From 2d10f305787cdb3f64716c5295721365c9ed1a90 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 14 Jul 2025 16:53:47 -0400 Subject: [PATCH 04/13] Add if statements for cylinder angle --- Shaders/Private/CesiumCylinder.usf | 49 ++++++++++++------- .../Private/CesiumVoxelRendererComponent.cpp | 16 +++--- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/Shaders/Private/CesiumCylinder.usf b/Shaders/Private/CesiumCylinder.usf index c1400ce44..ebc904785 100644 --- a/Shaders/Private/CesiumCylinder.usf +++ b/Shaders/Private/CesiumCylinder.usf @@ -36,6 +36,7 @@ struct Cylinder // Flags are packed in CesiumVoxelRendererComponent.cpp RadiusHasMinimumBound = bool(PackedData0.x); + RadiusIsFlat = bool(PackedData0.y); AngleRangeFlag = round(PackedData1.x); AngleMinHasDiscontinuity = bool(PackedData1.y); @@ -124,12 +125,12 @@ struct Cylinder RayIntersections OuterResult = IntersectBoundedCylinder(R, MaxBounds.x, float2(MinBounds.z, MaxBounds.z)); setShapeIntersections(Intersections, ListState, 0, OuterResult); - if (OuterResult.Entry.t == NO_HIT) { return; } + ListState.Length += 2; if (RadiusIsFlat) { // When the cylinder is perfectly thin it's necessary to sandwich the @@ -157,24 +158,36 @@ struct Cylinder else if (RadiusHasMinimumBound) { RayIntersections InnerResult = IntersectInfiniteCylinder(R, MinBounds.x, false); - setShapeIntersection(Intersections, ListState, 1, InnerResult); + setShapeIntersections(Intersections, ListState, 1, InnerResult); + } + + float2 AngleBounds = float2(MinBounds.y, MaxBounds.y); + if (AngleRangeFlag == ANGLE_UNDER_HALF) + { + // The shape's angle range is over half, so we intersect a NEGATIVE shape that is under half. + RayIntersections WedgeResult = IntersectRegularWedge(R, AngleBounds); + setShapeIntersections(Intersections, ListState, 2, WedgeResult); + ListState.Length += 2; + } + else if (AngleRangeFlag == ANGLE_OVER_HALF) + { + // The shape's angle range is under half, so we intersect a NEGATIVE shape that is over half. + RayIntersections FirstResult = (RayIntersections) 0; + RayIntersections SecondResult = (RayIntersections) 0; + IntersectFlippedWedge(R, AngleBounds, FirstResult, SecondResult); + setShapeIntersections(Intersections, ListState, 2, FirstResult); + setShapeIntersections(Intersections, ListState, 3, SecondResult); + ListState.Length += 4; + } + else if (AngleRangeFlag == ANGLE_EQUAL_ZERO) + { + RayIntersections FirstResult = (RayIntersections) 0; + RayIntersections SecondResult = (RayIntersections) 0; + IntersectHalfPlane(R, AngleBounds.x, FirstResult, SecondResult); + setShapeIntersections(Intersections, ListState, 2, FirstResult); + setShapeIntersections(Intersections, ListState, 3, SecondResult); + ListState.Length += 4; } - - // TODO -#if defined(CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_UNDER_HALF) - RayShapeIntersection wedgeIntersect = intersectRegularWedge(ray, u_cylinderRenderAngleMinMax); - setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_ANGLE, wedgeIntersect); -#elif defined(CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_OVER_HALF) - RayShapeIntersection wedgeIntersects[2]; - intersectFlippedWedge(ray, u_cylinderRenderAngleMinMax, wedgeIntersects); - setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_ANGLE + 0, wedgeIntersects[0]); - setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_ANGLE + 1, wedgeIntersects[1]); -#elif defined(CYLINDER_HAS_RENDER_BOUNDS_ANGLE_RANGE_EQUAL_ZERO) - RayShapeIntersection wedgeIntersects[2]; - intersectHalfPlane(ray, u_cylinderRenderAngleMinMax.x, wedgeIntersects); - setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_ANGLE + 0, wedgeIntersects[0]); - setShapeIntersection(ix, CYLINDER_INTERSECTION_INDEX_ANGLE + 1, wedgeIntersects[1]); -#endif } /** diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index a8cae7619..f3b0e949a 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -145,22 +145,22 @@ enum AngleDescription : int8 { OverHalf = 4 }; -AngleDescription interpretCylinderAngle(double value) { +AngleDescription interpretCylinderRange(double value) { const double angleEpsilon = CesiumUtility::Math::Epsilon10; - if (value > angleEpsilon && + if (value >= CesiumUtility::Math::OnePi - angleEpsilon && value < CesiumUtility::Math::TwoPi - angleEpsilon) { // angle range > PI return AngleDescription::OverHalf; } - if (value < CesiumUtility::Math::OnePi - angleEpsilon) { + if (value > angleEpsilon && + value < CesiumUtility::Math::OnePi - angleEpsilon) { // angle range < PI return AngleDescription::UnderHalf; } - if (value >= CesiumUtility::Math::OnePi - angleEpsilon && - value <= CesiumUtility::Math::OnePi + angleEpsilon) { - // angle range ~= PI - return AngleDescription::Half; + if (value <= angleEpsilon) { + // angle range ~= 0 + return AngleDescription::Zero; } return AngleDescription::None; @@ -223,7 +223,7 @@ void setVoxelCylinderProperties( bool isAngleReversed = angularBounds.y < angularBounds.x; double angleRange = angularBounds.y - angularBounds.x + isAngleReversed * defaultRange; - AngleDescription angleRangeIndicator = interpretCylinderAngle(angleRange); + AngleDescription angleRangeIndicator = interpretCylinderRange(angleRange); // Refers to the discontinuity at angle -pi / pi. const double discontinuityEpsilon = From c7f8d7366e6373e779023265c3f3afd7feabaf2a Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 30 Jul 2025 17:34:23 -0400 Subject: [PATCH 05/13] Some math fixes [skip ci] --- .../Materials/Layers/ML_CesiumVoxel.uasset | Bin 47932 -> 54148 bytes Shaders/Private/CesiumCylinder.usf | 17 +++++++++------- Shaders/Private/CesiumRayIntersectionList.usf | 4 ++-- Shaders/Private/CesiumShape.usf | 14 ++++++------- Shaders/Private/CesiumVoxelTemplate.usf | 19 +++++++++--------- .../Private/CesiumVoxelRendererComponent.cpp | 8 ++++---- 6 files changed, 32 insertions(+), 30 deletions(-) diff --git a/Content/Materials/Layers/ML_CesiumVoxel.uasset b/Content/Materials/Layers/ML_CesiumVoxel.uasset index 28e011b07474b0ae709dd285423b8cd819d0ee4f..6a975c316448e28397022f0d78be5940aff5fe30 100644 GIT binary patch literal 54148 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 UfS=qS{o<`p>~qFO$As|z4<#3282|tP delta 13750 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 diff --git a/Shaders/Private/CesiumCylinder.usf b/Shaders/Private/CesiumCylinder.usf index ebc904785..1156efa03 100644 --- a/Shaders/Private/CesiumCylinder.usf +++ b/Shaders/Private/CesiumCylinder.usf @@ -119,18 +119,20 @@ struct Cylinder /** * Tests whether the input ray (Unit Space) intersects the cylinder. Outputs the intersections in Unit Space. */ - void IntersectCylinder(in Ray R, out float4 Intersections[INTERSECTIONS_LENGTH], inout IntersectionListState ListState) + void Intersect(in Ray R, out float4 Intersections[INTERSECTIONS_LENGTH], inout IntersectionListState ListState) { ListState.Length = 2; RayIntersections OuterResult = IntersectBoundedCylinder(R, MaxBounds.x, float2(MinBounds.z, MaxBounds.z)); setShapeIntersections(Intersections, ListState, 0, OuterResult); + if (OuterResult.Entry.t == NO_HIT) { return; } - + ListState.Length += 2; + if (RadiusIsFlat) { // When the cylinder is perfectly thin it's necessary to sandwich the @@ -160,6 +162,7 @@ struct Cylinder RayIntersections InnerResult = IntersectInfiniteCylinder(R, MinBounds.x, false); setShapeIntersections(Intersections, ListState, 1, InnerResult); } + return; float2 AngleBounds = float2(MinBounds.y, MaxBounds.y); if (AngleRangeFlag == ANGLE_UNDER_HALF) @@ -205,7 +208,7 @@ struct Cylinder float angle = UV.y * CZM_TWO_PI; if (AngleRangeFlag > 0) { - angle /= AngleUVScale; + // angle /= AngleUVScale; } return float3(radius, angle, UV.z); @@ -247,23 +250,23 @@ struct Cylinder // Technically this should compare against min angle, but it has precision problems so compare against the middle of empty space. if (AngleIsReversed) { - angleUV += float(angleUV < AngleUVExtents.z); + //angleUV += float(angleUV < AngleUVExtents.z); } // Avoid flickering from reading voxels from both sides of the -pi/+pi discontinuity. if (AngleMinHasDiscontinuity) { - angleUV = angleUV > AngleUVExtents.z ? AngleUVExtents.x : angleUV; + //angleUV = angleUV > AngleUVExtents.z ? AngleUVExtents.x : angleUV; } if (AngleMaxHasDiscontinuity) { - angleUV = angleUV < AngleUVExtents.z ? AngleUVExtents.y : angleUV; + //angleUV = angleUV < AngleUVExtents.z ? AngleUVExtents.y : angleUV; } if (AngleRangeFlag > 0) { - angleUV = angleUV * AngleUVScale + AngleUVOffset; + //angleUV = angleUV * AngleUVScale + AngleUVOffset; } return float3(radiusUV, angleUV, height); diff --git a/Shaders/Private/CesiumRayIntersectionList.usf b/Shaders/Private/CesiumRayIntersectionList.usf index 318484600..682d94201 100644 --- a/Shaders/Private/CesiumRayIntersectionList.usf +++ b/Shaders/Private/CesiumRayIntersectionList.usf @@ -2,12 +2,12 @@ // Copyright 2020-2024 CesiumGS, Inc. and Contributors -#include "CesiumRayIntersection.usf" - /*=========================== CesiumRayIntersectionList.usf: Utility for tracking ray intersections across a complex shape. =============================*/ +#include "CesiumRayIntersection.usf" + // SHAPE_INTERSECTIONS is the number of ray-*shape* intersections (i.e., the volume intersection pairs), // INTERSECTIONS_LENGTH is the number of ray-*surface* intersections. #define SHAPE_INTERSECTIONS 7 diff --git a/Shaders/Private/CesiumShape.usf b/Shaders/Private/CesiumShape.usf index 3a3d27b73..fc61020c6 100644 --- a/Shaders/Private/CesiumShape.usf +++ b/Shaders/Private/CesiumShape.usf @@ -6,10 +6,10 @@ CesiumShape.usf: An implicit shape that can be intersected by a ray. =============================================================================*/ -#include "CesiumShapeConstants.usf" #include "CesiumBox.usf" #include "CesiumCylinder.usf" #include "CesiumEllipsoidRegion.usf" +#include "CesiumShapeConstants.usf" struct Shape { @@ -35,8 +35,8 @@ struct Shape BoxShape.MaxBounds = 1; break; case CYLINDER: - CylinderShape.MinBounds = InMinBounds; // radius, angle, height - CylinderShape.MaxBounds = InMaxBounds; // radius, angle, height + CylinderShape.Initialize(InMinBounds, InMaxBounds, PackedData0, PackedData1, PackedData2); + break; case ELLIPSOID: RegionShape.Initialize(InMinBounds, InMaxBounds, PackedData0, PackedData1, PackedData2, PackedData3, PackedData4, PackedData5); break; @@ -55,7 +55,7 @@ struct Shape BoxShape.Intersect(R, Intersections, ListState); break; case CYLINDER: - CylinderShape.IntersectCylinder(R, Intersections, ListState); + CylinderShape.Intersect(R, Intersections, ListState); break; case ELLIPSOID: RegionShape.Intersect(R, Intersections, ListState); @@ -67,14 +67,12 @@ struct Shape RayIntersections result = ListState.GetFirstIntersections(Intersections); if (result.Entry.t == NO_HIT) { + // Don't bother with sorting if the positive shape was missed. return result; } - // Don't bother with sorting if the positive shape was missed. // Box intersection is straightforward and doesn't require sorting. - // Currently, cylinders do not require sorting, but they will once clipping / exaggeration - // is supported. - if (ShapeConstant == ELLIPSOID) + if (ShapeConstant != BOX) { ListState.Sort(Intersections); for (int i = 0; i < SHAPE_INTERSECTIONS; ++i) diff --git a/Shaders/Private/CesiumVoxelTemplate.usf b/Shaders/Private/CesiumVoxelTemplate.usf index f535f3458..be27f8edb 100644 --- a/Shaders/Private/CesiumVoxelTemplate.usf +++ b/Shaders/Private/CesiumVoxelTemplate.usf @@ -68,15 +68,12 @@ struct VoxelMegatextures float3 LocalUV = Sample.LocalUV; uint3 DataDimensions = GridDimensions + PaddingBefore + PaddingAfter; - float3 VoxelCoords = floor(LocalUV * float3(GridDimensions)); - // Account for padding - VoxelCoords = clamp(VoxelCoords + float3(PaddingBefore), 0, float3(DataDimensions - 1u)); - if (ShapeConstant == BOX) { // Since glTFs are y-up (and 3D Tiles is z-up), the data must be accessed to reflect the transforms // from a Y-up to Z-up frame of reference, plus the Cesium -> Unreal transform as well. - VoxelCoords = float3(VoxelCoords.x, DataDimensions.y - VoxelCoords.z, VoxelCoords.y); + // VoxelCoords = float3(VoxelCoords.x, (DataDimensions.y - 1u) - VoxelCoords.z, VoxelCoords.y); + LocalUV = float3(LocalUV.x, clamp(1.0 - LocalUV.z, 0.0, 1.0), LocalUV.y); } else if (ShapeConstant == CYLINDER) { @@ -84,10 +81,15 @@ struct VoxelMegatextures // (i.e., the root tile only). // TODO: This assumes that the root tile is a whole cylinder, but the root tile may be // a partial cylinder. - float Angle = (Sample.Coords.w == 0) ? (VoxelCoords.y + DataDimensions.z / 2.0) % DataDimensions.z : VoxelCoords.y; - VoxelCoords = float3(VoxelCoords.x, VoxelCoords.z, Angle); + LocalUV = float3(LocalUV.x, frac(LocalUV.z + 0.5), LocalUV.y); + //float Angle = (Sample.Coords.w == 0) ? (VoxelCoords.y + (float)DataDimensions.z / 2.0) % DataDimensions.z : VoxelCoords.y; + //VoxelCoords = float3(VoxelCoords.x, VoxelCoords.z, Angle); } + float3 VoxelCoords = floor(LocalUV * float3(GridDimensions)); + // Account for padding + VoxelCoords = clamp(VoxelCoords + float3(PaddingBefore), 0, float3(DataDimensions - 1u)); + int3 Coords = TileCoords + VoxelCoords; CustomShaderProperties Properties = (CustomShaderProperties) 0; @@ -235,8 +237,7 @@ for (int step = 0; step < STEP_COUNT_MAX; step++) { // Keep raymarching CurrentT += NextIntersection.t; if (CurrentT > EndT) { - if (ShapeConstant == ELLIPSOID) { - // For CYLINDER: once clipping / exaggeration is supported, this must be done for cylinders too. + if (ShapeConstant != BOX) { Intersections = Octree.GridShape.ListState.GetNextIntersections(IntersectionList); if (Intersections.Entry.t == NO_HIT) { break; diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index f3b0e949a..007c05837 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -196,13 +196,13 @@ void setVoxelCylinderProperties( // Radius { - double radiusRange = radialBounds.y - radialBounds.x; + double normalizedRadiusRange = 1.0 - normalizedMinimumRadius; bool hasNonzeroMinimumRadius = normalizedMinimumRadius > 0.0; bool hasFlatRadius = radialBounds.x == radialBounds.y; - if (hasNonzeroMinimumRadius && radiusRange > 0.0) { - radiusUVScale = 1.0 / radiusRange; - radiusUVOffset = -radialBounds.x / radiusRange; + if (hasNonzeroMinimumRadius && normalizedRadiusRange > 0.0) { + radiusUVScale = 1.0 / normalizedRadiusRange; + radiusUVOffset = -normalizedMinimumRadius / normalizedRadiusRange; } radiusFlags.X = hasNonzeroMinimumRadius; From 56cb713fdc4227a618638b620733ef2d9337b618 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 1 Aug 2025 14:44:23 -0400 Subject: [PATCH 06/13] More math fixes [skip ci] --- .../Materials/Layers/ML_CesiumVoxel.uasset | Bin 54148 -> 54194 bytes Shaders/Private/CesiumCylinder.usf | 30 +++--- Shaders/Private/CesiumShape.usf | 1 + Shaders/Private/CesiumVoxelTemplate.usf | 11 ++- .../Private/CesiumVoxelRendererComponent.cpp | 90 ++++++++++++++---- Source/CesiumRuntime/Private/VecMath.cpp | 4 + Source/CesiumRuntime/Private/VecMath.h | 10 ++ 7 files changed, 105 insertions(+), 41 deletions(-) diff --git a/Content/Materials/Layers/ML_CesiumVoxel.uasset b/Content/Materials/Layers/ML_CesiumVoxel.uasset index 6a975c316448e28397022f0d78be5940aff5fe30..8185c66e84e8b6250ba310eb742213f1ef8f2c74 100644 GIT binary patch delta 7668 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 HBkKPER7xv` delta 7754 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 Kp>|$h&i??=NngDH diff --git a/Shaders/Private/CesiumCylinder.usf b/Shaders/Private/CesiumCylinder.usf index 1156efa03..0ddb3aff9 100644 --- a/Shaders/Private/CesiumCylinder.usf +++ b/Shaders/Private/CesiumCylinder.usf @@ -131,8 +131,6 @@ struct Cylinder return; } - ListState.Length += 2; - if (RadiusIsFlat) { // When the cylinder is perfectly thin it's necessary to sandwich the @@ -156,23 +154,18 @@ struct Cylinder setSurfaceIntersection(Intersections, ListState, 1, InnerResult.Entry, false, true); // negative, entering setSurfaceIntersection(Intersections, ListState, 2, InnerResult.Exit, false, false); // negative, exiting setSurfaceIntersection(Intersections, ListState, 3, OuterResult.Exit, true, false); // positive, exiting + ListState.Length += 2; + } else if (RadiusHasMinimumBound) { RayIntersections InnerResult = IntersectInfiniteCylinder(R, MinBounds.x, false); setShapeIntersections(Intersections, ListState, 1, InnerResult); + ListState.Length += 2; } - return; float2 AngleBounds = float2(MinBounds.y, MaxBounds.y); if (AngleRangeFlag == ANGLE_UNDER_HALF) - { - // The shape's angle range is over half, so we intersect a NEGATIVE shape that is under half. - RayIntersections WedgeResult = IntersectRegularWedge(R, AngleBounds); - setShapeIntersections(Intersections, ListState, 2, WedgeResult); - ListState.Length += 2; - } - else if (AngleRangeFlag == ANGLE_OVER_HALF) { // The shape's angle range is under half, so we intersect a NEGATIVE shape that is over half. RayIntersections FirstResult = (RayIntersections) 0; @@ -182,6 +175,13 @@ struct Cylinder setShapeIntersections(Intersections, ListState, 3, SecondResult); ListState.Length += 4; } + else if (AngleRangeFlag == ANGLE_OVER_HALF) + { + // The shape's angle range is over half, so we intersect a NEGATIVE shape that is under half. + RayIntersections WedgeResult = IntersectRegularWedge(R, AngleBounds); + setShapeIntersections(Intersections, ListState, 2, WedgeResult); + ListState.Length += 2; + } else if (AngleRangeFlag == ANGLE_EQUAL_ZERO) { RayIntersections FirstResult = (RayIntersections) 0; @@ -208,7 +208,7 @@ struct Cylinder float angle = UV.y * CZM_TWO_PI; if (AngleRangeFlag > 0) { - // angle /= AngleUVScale; + angle /= AngleUVScale; } return float3(radius, angle, UV.z); @@ -250,23 +250,23 @@ struct Cylinder // Technically this should compare against min angle, but it has precision problems so compare against the middle of empty space. if (AngleIsReversed) { - //angleUV += float(angleUV < AngleUVExtents.z); + angleUV += float(angleUV < AngleUVExtents.z); } // Avoid flickering from reading voxels from both sides of the -pi/+pi discontinuity. if (AngleMinHasDiscontinuity) { - //angleUV = angleUV > AngleUVExtents.z ? AngleUVExtents.x : angleUV; + angleUV = angleUV > AngleUVExtents.z ? AngleUVExtents.x : angleUV; } if (AngleMaxHasDiscontinuity) { - //angleUV = angleUV < AngleUVExtents.z ? AngleUVExtents.y : angleUV; + angleUV = angleUV < AngleUVExtents.z ? AngleUVExtents.y : angleUV; } if (AngleRangeFlag > 0) { - //angleUV = angleUV * AngleUVScale + AngleUVOffset; + angleUV = angleUV * AngleUVScale + AngleUVOffset; } return float3(radiusUV, angleUV, height); diff --git a/Shaders/Private/CesiumShape.usf b/Shaders/Private/CesiumShape.usf index fc61020c6..f14092d9d 100644 --- a/Shaders/Private/CesiumShape.usf +++ b/Shaders/Private/CesiumShape.usf @@ -119,6 +119,7 @@ struct Shape */ float3 ConvertUVToShapeUVSpace(in float3 UVPosition, out float3x3 JacobianT) { + [branch] switch (ShapeConstant) { case BOX: diff --git a/Shaders/Private/CesiumVoxelTemplate.usf b/Shaders/Private/CesiumVoxelTemplate.usf index be27f8edb..805a520c7 100644 --- a/Shaders/Private/CesiumVoxelTemplate.usf +++ b/Shaders/Private/CesiumVoxelTemplate.usf @@ -142,7 +142,7 @@ R.Direction = RayDirection; // // Shape UV Space is the [0, 1] grid mapping that conforms to the actual voxel volume. Imagine // the voxel grid curving concentrically around a unit cylinder, then being "smooshed" to fit in -// the volume of the hollow cylinder. Shape UV Space must account for the angle bounds of a cylinder, +// the volume of the hollow cylinder. Shape UV Space must account for the radial and angle bounds of a cylinder, // and the longitude / latitude / height bounds of an ellipsoid. // // Therefore, we must convert the unit space ray to the equivalent Shape UV Space ray to sample correctly. @@ -239,19 +239,20 @@ for (int step = 0; step < STEP_COUNT_MAX; step++) { if (CurrentT > EndT) { if (ShapeConstant != BOX) { Intersections = Octree.GridShape.ListState.GetNextIntersections(IntersectionList); - if (Intersections.Entry.t == NO_HIT) { - break; - } else { + if (Intersections.Entry.t != NO_HIT) { // Found another intersection. Resume raymarching there CurrentT = Intersections.Entry.t; EndT = Intersections.Exit.t; + } else { + break; } } else { break; } } - PositionUV = R.Origin + CurrentT * R.Direction; + PositionUV = R.Origin + CurrentT * R. +Direction; PositionShapeUVSpace = Octree.GridShape.ConvertUVToShapeUVSpace(PositionUV, JacobianT); Octree.ResumeTraversal(PositionShapeUVSpace, Traversal, Sample); NextIntersection = Octree.GetNextVoxelIntersection(Sample, RawDirection, Intersections, JacobianT, CurrentT); diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 007c05837..c0afee584 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -17,10 +17,12 @@ #include "VecMath.h" #include +#include #include #include #include #include +#include #include // Sets default values for this component's properties @@ -150,7 +152,7 @@ AngleDescription interpretCylinderRange(double value) { if (value >= CesiumUtility::Math::OnePi - angleEpsilon && value < CesiumUtility::Math::TwoPi - angleEpsilon) { - // angle range > PI + // angle range >= PI return AngleDescription::OverHalf; } if (value > angleEpsilon && @@ -217,12 +219,13 @@ void setVoxelCylinderProperties( double angleUVScale = 1.0; double angleUVOffset = 0.0; FIntVector4 angleFlags(0); + + const double defaultAngleRange = CesiumUtility::Math::TwoPi; + bool isAngleReversed = angularBounds.y < angularBounds.x; + double angleRange = + angularBounds.y - angularBounds.x + isAngleReversed * defaultAngleRange; // Angle { - const double defaultRange = CesiumUtility::Math::TwoPi; - bool isAngleReversed = angularBounds.y < angularBounds.x; - double angleRange = - angularBounds.y - angularBounds.x + isAngleReversed * defaultRange; AngleDescription angleRangeIndicator = interpretCylinderRange(angleRange); // Refers to the discontinuity at angle -pi / pi. @@ -230,11 +233,11 @@ void setVoxelCylinderProperties( CesiumUtility::Math::Epsilon3; // 0.001 radians = 0.05729578 degrees bool angleMinimumHasDiscontinuity = CesiumUtility::Math::equalsEpsilon( angularBounds.x, - -CesiumUtility::Math::OnePi, + CesiumUtility::Math::OnePi, discontinuityEpsilon); bool angleMaximumHasDiscontinuity = CesiumUtility::Math::equalsEpsilon( angularBounds.y, - CesiumUtility::Math::OnePi, + -CesiumUtility::Math::OnePi, discontinuityEpsilon); angleFlags.X = angleRangeIndicator; @@ -244,13 +247,13 @@ void setVoxelCylinderProperties( // Compute the extents of the angle range in UV Shape Space. double minimumAngleUV = - (angularBounds.x - defaultMinimumBounds.Y) / defaultRange; + (angularBounds.x - defaultMinimumBounds.Y) / defaultAngleRange; double maximumAngleUV = - (angularBounds.y - defaultMinimumBounds.Y) / defaultRange; - // Given an angle range, represents the actual value where "0" would be - // in UV coordinates. - double angleRangeUVZero = 1.0 - angleRange / defaultRange; - // TODO: document this + (angularBounds.y - defaultMinimumBounds.Y) / defaultAngleRange; + // Given the angle range, describes the proportion of the cylinder + // that is excluded from that range. + double angleRangeUVZero = 1.0 - angleRange / defaultAngleRange; + // Describes the midpoint of the above excluded range. double angleRangeUVZeroMid = glm::fract(maximumAngleUV + 0.5 * angleRangeUVZero); @@ -259,7 +262,7 @@ void setVoxelCylinderProperties( const double angleEpsilon = CesiumUtility::Math::Epsilon10; if (angleRange > angleEpsilon) { - angleUVScale = defaultRange / angleRange; + angleUVScale = defaultAngleRange / angleRange; angleUVOffset = -(angularBounds.x - defaultMinimumBounds.Y) / angleRange; } } @@ -308,28 +311,73 @@ void setVoxelCylinderProperties( 0), FVector4(radiusUVScale, radiusUVOffset, angleUVScale, angleUVOffset)); - // The transform and scale of the cylinder are handled in the component's - // transform, so there is no need to duplicate it here. Instead, this - // transform is configured to scale the engine-provided Cube ([-50, 50]) to - // unit space ([-1, 1]). + // Distinct from the component's transform above, this scales from the + // engine-provided Cube's space ([-50, 50]) to a unit space of [-1, 1]. This + // is specifically used to fit the raymarched cube into the bounds of the + // explicit cube mesh. In other words, this scale must be applied in-shader + // to account for the actual mesh's bounds. + glm::dmat4 localToUnit(0.02); + localToUnit[3][3] = 1.0; + + // With cylinder regions, the scale of tight-fitting bounding boxes will vary + // for partial cylinders. + if (angleRange < defaultAngleRange) { + glm::dvec3 scale; + CesiumGeometry::Transforms::computeTranslationRotationScaleFromMatrix( + glm::dmat4( + glm::dvec4(halfAxes[0], 0.0), + glm::dvec4(halfAxes[1], 0.0), + glm::dvec4(halfAxes[2], 0.0), + glm::dvec4(0.0, 0.0, 0.0, 1.0)), + nullptr, + nullptr, + &scale); + + // If the cylinder was whole, the scale would have been the maximum radius + // along the xy-plane. The scale correction is thus the proportion to + // original scale. + glm::dvec3 scaleCorrection( + scale.x / radialBounds.y, + scale.y / radialBounds.y, + 1.0); + const glm::dmat3& inverseHalfAxes = box.getInverseHalfAxes(); + // The offset that occurs as a result of the smaller scale can be deduced + // from the box's inverse transform. + glm::dvec3 worldOffset = box.getCenter() - cylinder.getTranslation(); + glm::dvec3 localOffset( + glm::dmat4( + glm::dvec4(inverseHalfAxes[0], 0.0), + glm::dvec4(inverseHalfAxes[1], 0.0), + glm::dvec4(inverseHalfAxes[2], 0.0), + glm::dvec4(0.0, 0.0, 0.0, 1.0)) * + glm::dvec4(worldOffset, 1.0)); + + localToUnit = + CesiumGeometry::Transforms::createTranslationRotationScaleMatrix( + scaleCorrection * localOffset, + glm::dquat(1.0, 0.0, 0.0, 0.0), + scaleCorrection) * + localToUnit; + } + pVoxelMaterial->SetVectorParameterValueByInfo( FMaterialParameterInfo( UTF8_TO_TCHAR("Shape TransformToUnit Row 0"), EMaterialParameterAssociation::LayerParameter, 0), - FVector4(0.02, 0, 0, 0)); + VecMath::createVector4(glm::row(localToUnit, 0))); pVoxelMaterial->SetVectorParameterValueByInfo( FMaterialParameterInfo( UTF8_TO_TCHAR("Shape TransformToUnit Row 1"), EMaterialParameterAssociation::LayerParameter, 0), - FVector4(0, 0.02, 0, 0)); + VecMath::createVector4(glm::row(localToUnit, 1))); pVoxelMaterial->SetVectorParameterValueByInfo( FMaterialParameterInfo( UTF8_TO_TCHAR("Shape TransformToUnit Row 2"), EMaterialParameterAssociation::LayerParameter, 0), - FVector4(0, 0, 0.02, 0)); + VecMath::createVector4(glm::row(localToUnit, 2))); } AngleDescription interpretLongitudeRange(double value) { diff --git a/Source/CesiumRuntime/Private/VecMath.cpp b/Source/CesiumRuntime/Private/VecMath.cpp index cbfa08d83..dc26e2f81 100644 --- a/Source/CesiumRuntime/Private/VecMath.cpp +++ b/Source/CesiumRuntime/Private/VecMath.cpp @@ -167,6 +167,10 @@ FVector VecMath::createVector(const glm::dvec3& v) noexcept { return FVector(v.x, v.y, v.z); } +FVector4 VecMath::createVector4(const glm::dvec4& v) noexcept { + return FVector4(v.x, v.y, v.z, v.w); +} + FRotator VecMath::createRotator(const glm::dmat4& m) noexcept { // Avoid converting to Unreal single-precision types until the very end, so // that all intermediate conversions are performed in double-precision. diff --git a/Source/CesiumRuntime/Private/VecMath.h b/Source/CesiumRuntime/Private/VecMath.h index f3fe7da00..2634b9ace 100644 --- a/Source/CesiumRuntime/Private/VecMath.h +++ b/Source/CesiumRuntime/Private/VecMath.h @@ -194,6 +194,16 @@ class VecMath { */ static FVector createVector(const glm::dvec3& v) noexcept; + + /** + * @brief Create an `FVector4` from the given `glm` 4D vector. + * + * @param v The `glm` vector. + * @return The `FVector4`. + * + */ + static FVector4 createVector4(const glm::dvec4& v) noexcept; + /** * @brief Create a `FRotator` from the given `glm` matrix. * From 30935acf8bbf4f3952e87f47f70f4a6b7ff3ee57 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 1 Aug 2025 17:45:56 -0400 Subject: [PATCH 07/13] Add missing shader parameters, other math fixes --- .../Materials/Layers/ML_CesiumVoxel.uasset | Bin 54194 -> 54245 bytes Shaders/Private/CesiumCylinder.usf | 47 +++++++++--------- Shaders/Private/CesiumShape.usf | 2 +- .../Private/CesiumVoxelRendererComponent.cpp | 22 +++++--- 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/Content/Materials/Layers/ML_CesiumVoxel.uasset b/Content/Materials/Layers/ML_CesiumVoxel.uasset index 8185c66e84e8b6250ba310eb742213f1ef8f2c74..92786610c1571a104a215194a9ef725044e082cc 100644 GIT binary patch delta 4574 zcma)<3tZGy7RP5m6sGwnlL`_LV}ptcNT%tU%q!x$ny%&=EI^sgR#yN zb&aTRin?cC(nCc(UDRtteNfb`qW($L&-EkAC{d@2x=_>&qW-pD0EBq;)t2GxeX!-} z_BUM1R?*8uy?|Dwly--r$st8s3YA`ivHo1t2}!;%HA#>CIPc+J+t8YF;*OtqnedEA z_(MIg=71|=RJf-DelSUedpY3AQ&gCOdOWb* zu`2B0fIH$O4uiSAo{;IS*Oj0Nl`&HVD)KeM-N)p1yR7(`Adq-!tUYj7SGf{3w z`H?)QKg^cn2YK9=(tUWv&sR4~CjR@)!eFky8AAN^x-V&gSR!+T+~RNcD#R{l)Ibd^ z0fhql2iP%ZG?p4z!gkwdea?d2fEkK#Nr`f$`$~oU5apHt zz3#s>5te@*5^|c+4B^JUE_chg>$M9hA0)QEQ?9~%kQm=Otil{N<1de>Fo(_fdW8z} zL1KKRQib^-F}`qAh4~;cK2;^~U6UCqjC$RhGAk#oLY^Cl`xmI!*`yp`6LN)j5Znvw z4@HPZNYr**pzwiaPzLICy`g=e9}Y5m?SCx$jDM_G;iu4q!hDr>3Cq|M0=Eygd+Sjm93*jOgTNS5XrPoj@i8J# zDx>hTA3q9y$Ze*yDQWC-OZ-1>PfNF28OpqQmjgTz>ONrm|!F*aOQVZNM<`?shtU(Ohr zx`0-J{X_5;;>MR?TBxL46>@Hf+3TQP(a^vz%WNsvAY<*b@DMS3(B==8IJ!;Xv`{?T zUhW2(DngF3guW`FDb^dRCij4OiSD+J(C_-VEx_;Jg|;suY+CI~xD^=;b&)}$m4|gg zK@jY;M2LP6xJCub*376ylHQ48`r1UMr4yOH6wS0an(0r`OgF?Z?TlgC8Pfw|CcP;} z9|G4WO^`E=pUf8TPiBA96!y=X!u0DY$x0QTz-Sq@J%(S$vSoWLTaJiJR=&c+sJ3j1 zW6R)pw#<%CR*VKmOV6pRWe%#lTFD6!+7-5;fiz2=k+L}1Mxji4i&hkAQ0 zw{CZ}YGwq((nKFPw2s5RwT`XcSkJBYuGZ`gq**s)VoeE{@gCb;?`lo^BWu^XTJalM zYw2oD-efVXatgnAQwpwKw>b_nHv71IrGtz(U&xxOrx-EjzifU>woH?8n&{&c&k-vmVDOs<=ZtHi+n%W zUIweT`M6~19Ts(Z$>3_EBS54&ZTmhOLXjtQ!Ev+H-eX`vXdho{WuMx4UrF;MpG6Kvc= zR>gb5;Ke<3V`$PrA4pGaAGo+@7%a~1i$uLJw<5Bku-_*Yjvn zhQE8=U`dV-yj3VB6uZ)$0eS#`X$Imy>m6o zERA-@c|--lXr3EyMn;NE58J zZ4$%;!;jM~r9_@O&SQmE^W(j)I!4la)jVo;O_Y-56vTrX_BJO4YS%zcZJ0-<+?KU) zrZx=nH@SoRULP>V`YWAI+nrL!{*j5~|L?jmxL5m%GSA6!&qriwZ3u#%^}mvRpGvCAVR-W}2)^P8V;ZA}Bpbtx6Vl&)ccC$9y3w+7`NH(&%Zx#3sp-MSwD~Ks zN?ovYNoxA?A&}iLTueCXu(faqbTo}rO?#83y&mQ;;RwLN;xULju!gy vXX|w>RqOHF$JgTdkG%V1+E&k-Sbxu>sc7`bMPO}<)vkj_ZGKq$eOmZGfyGXF delta 4404 zcma)93s93+7S4}Q6V2Zxg%~IxU_>4TBo=mOEmA;&JOvgNB|6=>c6WVCySDVCTo6w{iPr7>Jm%p#;rZL^lDUWs--FV;`F3lWv$+`?!9}B`DA_$f=63bt`71&XmS$4@)qH5?b!BmLBJe327g$MI$-d^g8` zQTxO9CGrtD>;>Hft=nD9<^W2;f|;#_JFv*ZA8I1yZmbMt%$}{_UlRg`&<}^uAB#O< zjfYaw@S326Y>h80^ie9Zc$163q%lDEFr*g-GzK-G?=d*#I7v4Ej9w1L)rnaRy_{h| z4YxT13El>8=IuErModQlEE)~eXyT>)#yV*%guoXa`!SEhy&eP$XV?)L&O8Lc#{jXu zN<{-QY@|W~B8P?{58x&uzJ5x@vT+U_fE-_g`XgMp;AH;1_!<8N?7lM3ztPlj17$b1_%#OD(3K*x}X=4ngCD14_X5-%gIW`L$)yv4Fm=n)Kw@^mc*#Q z!Wrs0G2=m)$w5RgeL`L1ZjPBEf`6#uTMu1xI3F0Fib?Ugk(W z7^k34gi?{hk?YVq#o%2(tY*acoC!z7$vS4GmSHi z5EwEuB9gEA@rE;fHezrxUm{LsF~H;a5-ezOpRb-}@jSOU&UG$<5bD_dH=OyIGbSkD z40W4wdKYeThPqAF>x7&zhd1grF{mt(E1kt$EOwfHW4%X%TvDhTg@ zX?b|3vyzGZJek;pS;SV%A~q<6SaS-o<`h@Bo3faD z|63|)+(;#j=rpoFn?`J8I!sjo9s7g7Q=a(YJnyX(BgOR?s;2f^w^9NC%N!usU1G+0*3-D8=V%t&?Et7!;qz)KYZ&BC08+$JG?sj2gMH{h5({UBe^msckCX z=j~#Cet2-TOVK%Ije$*qp4uByP?Xg0#9iIklkQh1FJ|L2aO8E*-#xMjy8p8mdw~A> zz09NbTE6e;UVXfLy^VU5*g<@C{~x$?a{qM2CVrY7Ee>j;M_yYKJ)Zmz5O(+g!v+q# z4oQdo#9ozij{6UijmM!GLO@DDc19b6hrm#D0pIv1BH#QX`D^cw`HL2Ra=y>4x$bPKu5YC#GW9Xf)h-qr6UBs zIq`cbj!<~@WDZJ}lPu1bjxd;VialxBVc2wvvJ~1W*0OfO@cXVh`-yG*{8bW?mcx(Qo;g|ORK2z%oy!<A zcyJXfV%2`Sg*BC(f$KeZVyv3pQb8eAn)Y_rxYTQWzw}GlGpF;)uGd}}gdeVDPArjv UbNsN+VCgdwla)pZWPMigKRzGJ4*&oF diff --git a/Shaders/Private/CesiumCylinder.usf b/Shaders/Private/CesiumCylinder.usf index 0ddb3aff9..fcd07ea92 100644 --- a/Shaders/Private/CesiumCylinder.usf +++ b/Shaders/Private/CesiumCylinder.usf @@ -29,7 +29,7 @@ struct Cylinder bool AngleMaxHasDiscontinuity; bool AngleIsReversed; - void Initialize(float3 InMinBounds, float3 InMaxBounds, float4 PackedData0, float4 PackedData1, float4 PackedData2) + void Initialize(float3 InMinBounds, float3 InMaxBounds, float4 PackedData0, float4 PackedData1, float4 PackedData2, float4 PackedData3) { MinBounds = InMinBounds; // radius, angle, height MaxBounds = InMaxBounds; // radius, angle, height @@ -42,11 +42,13 @@ struct Cylinder AngleMinHasDiscontinuity = bool(PackedData1.y); AngleMaxHasDiscontinuity = bool(PackedData1.z); AngleIsReversed = bool(PackedData1.w); + + AngleUVExtents = PackedData2.xyz; - RadiusUVScale = PackedData2.x; - RadiusUVOffset = PackedData2.y; - AngleUVScale = PackedData2.z; - AngleUVOffset = PackedData2.w; + RadiusUVScale = PackedData3.x; + RadiusUVOffset = PackedData3.y; + AngleUVScale = PackedData3.z; + AngleUVOffset = PackedData3.w; } /** @@ -245,27 +247,26 @@ struct Cylinder // Angle: shift and scale to [0, 1] float angleUV = (angle + CZM_PI) / CZM_TWO_PI; - - // Correct the angle when max < min - // Technically this should compare against min angle, but it has precision problems so compare against the middle of empty space. - if (AngleIsReversed) - { - angleUV += float(angleUV < AngleUVExtents.z); - } - - // Avoid flickering from reading voxels from both sides of the -pi/+pi discontinuity. - if (AngleMinHasDiscontinuity) - { - angleUV = angleUV > AngleUVExtents.z ? AngleUVExtents.x : angleUV; - } - - if (AngleMaxHasDiscontinuity) - { - angleUV = angleUV < AngleUVExtents.z ? AngleUVExtents.y : angleUV; - } if (AngleRangeFlag > 0) { + // Correct the angle when max < min + // Technically this should compare against min angle, but it has precision problems so compare against the middle of empty space. + if (AngleIsReversed) + { + angleUV += float(angleUV < AngleUVExtents.z); + } + + // Avoid flickering from reading voxels from both sides of the -pi/+pi discontinuity. + if (AngleMinHasDiscontinuity) + { + angleUV = angleUV > AngleUVExtents.z ? AngleUVExtents.x : angleUV; + } + else if (AngleMaxHasDiscontinuity) + { + angleUV = angleUV < AngleUVExtents.z ? AngleUVExtents.y : angleUV; + } + angleUV = angleUV * AngleUVScale + AngleUVOffset; } diff --git a/Shaders/Private/CesiumShape.usf b/Shaders/Private/CesiumShape.usf index f14092d9d..e41a8f5bd 100644 --- a/Shaders/Private/CesiumShape.usf +++ b/Shaders/Private/CesiumShape.usf @@ -35,7 +35,7 @@ struct Shape BoxShape.MaxBounds = 1; break; case CYLINDER: - CylinderShape.Initialize(InMinBounds, InMaxBounds, PackedData0, PackedData1, PackedData2); + CylinderShape.Initialize(InMinBounds, InMaxBounds, PackedData0, PackedData1, PackedData2, PackedData3); break; case ELLIPSOID: RegionShape.Initialize(InMinBounds, InMaxBounds, PackedData0, PackedData1, PackedData2, PackedData3, PackedData4, PackedData5); diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index c0afee584..e96ef5a23 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -211,9 +211,9 @@ void setVoxelCylinderProperties( radiusFlags.Y = hasFlatRadius; } - // Defines the extents of the longitude in UV space. In other words, this - // expresses the minimum, maximum, and midpoint values of the longitude range - // in UV space. + // Defines the extents of the angle in UV space. In other words, this + // expresses the minimum and maximum values of the angle range, and the + // midpoint of the negative space (if it exists), all in UV space. FVector angleUVExtents = FVector::Zero(); double angleUVScale = 1.0; @@ -233,11 +233,11 @@ void setVoxelCylinderProperties( CesiumUtility::Math::Epsilon3; // 0.001 radians = 0.05729578 degrees bool angleMinimumHasDiscontinuity = CesiumUtility::Math::equalsEpsilon( angularBounds.x, - CesiumUtility::Math::OnePi, + -CesiumUtility::Math::OnePi, discontinuityEpsilon); bool angleMaximumHasDiscontinuity = CesiumUtility::Math::equalsEpsilon( angularBounds.y, - -CesiumUtility::Math::OnePi, + CesiumUtility::Math::OnePi, discontinuityEpsilon); angleFlags.X = angleRangeIndicator; @@ -302,13 +302,21 @@ void setVoxelCylinderProperties( 0), FVector4(angleFlags)); - // 2 = UV -> Shape UV Transforms (scale / offset) - // Radius (xy), Angle (zw) + // 2 = Angle UV extents (xyz) pVoxelMaterial->SetVectorParameterValueByInfo( FMaterialParameterInfo( UTF8_TO_TCHAR("Shape Packed Data 2"), EMaterialParameterAssociation::LayerParameter, 0), + angleUVExtents); + + // 3 = UV -> Shape UV Transforms (scale / offset) + // Radius (xy), Angle (zw) + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape Packed Data 3"), + EMaterialParameterAssociation::LayerParameter, + 0), FVector4(radiusUVScale, radiusUVOffset, angleUVScale, angleUVOffset)); // Distinct from the component's transform above, this scales from the From 94d6ec9071b97c52be395f5140ffebfd599badff Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 4 Aug 2025 15:12:16 -0400 Subject: [PATCH 08/13] has -> at discontinuity --- Shaders/Private/CesiumCylinder.usf | 12 ++++++------ .../Private/CesiumVoxelRendererComponent.cpp | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Shaders/Private/CesiumCylinder.usf b/Shaders/Private/CesiumCylinder.usf index fcd07ea92..8b2350b7c 100644 --- a/Shaders/Private/CesiumCylinder.usf +++ b/Shaders/Private/CesiumCylinder.usf @@ -25,8 +25,8 @@ struct Cylinder bool RadiusIsFlat; uint AngleRangeFlag; - bool AngleMinHasDiscontinuity; - bool AngleMaxHasDiscontinuity; + bool AngleMinAtDiscontinuity; + bool AngleMaxAtDiscontinuity; bool AngleIsReversed; void Initialize(float3 InMinBounds, float3 InMaxBounds, float4 PackedData0, float4 PackedData1, float4 PackedData2, float4 PackedData3) @@ -39,8 +39,8 @@ struct Cylinder RadiusIsFlat = bool(PackedData0.y); AngleRangeFlag = round(PackedData1.x); - AngleMinHasDiscontinuity = bool(PackedData1.y); - AngleMaxHasDiscontinuity = bool(PackedData1.z); + AngleMinAtDiscontinuity = bool(PackedData1.y); + AngleMaxAtDiscontinuity = bool(PackedData1.z); AngleIsReversed = bool(PackedData1.w); AngleUVExtents = PackedData2.xyz; @@ -258,11 +258,11 @@ struct Cylinder } // Avoid flickering from reading voxels from both sides of the -pi/+pi discontinuity. - if (AngleMinHasDiscontinuity) + if (AngleMinAtDiscontinuity) { angleUV = angleUV > AngleUVExtents.z ? AngleUVExtents.x : angleUV; } - else if (AngleMaxHasDiscontinuity) + else if (AngleMaxAtDiscontinuity) { angleUV = angleUV < AngleUVExtents.z ? AngleUVExtents.y : angleUV; } diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 7fb087d85..60ebb3541 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -231,18 +231,18 @@ void setVoxelCylinderProperties( // Refers to the discontinuity at angle -pi / pi. const double discontinuityEpsilon = CesiumUtility::Math::Epsilon3; // 0.001 radians = 0.05729578 degrees - bool angleMinimumHasDiscontinuity = CesiumUtility::Math::equalsEpsilon( + bool angleMinimumAtDiscontinuity = CesiumUtility::Math::equalsEpsilon( angularBounds.x, -CesiumUtility::Math::OnePi, discontinuityEpsilon); - bool angleMaximumHasDiscontinuity = CesiumUtility::Math::equalsEpsilon( + bool angleMaximumAtDiscontinuity = CesiumUtility::Math::equalsEpsilon( angularBounds.y, CesiumUtility::Math::OnePi, discontinuityEpsilon); angleFlags.X = angleRangeIndicator; - angleFlags.Y = angleMinimumHasDiscontinuity; - angleFlags.Z = angleMaximumHasDiscontinuity; + angleFlags.Y = angleMinimumAtDiscontinuity; + angleFlags.Z = angleMaximumAtDiscontinuity; angleFlags.W = isAngleReversed; // Compute the extents of the angle range in UV Shape Space. From 7d8adb0adaff9fa5d6f1ab89ad7de4e952aec052 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 4 Aug 2025 17:53:19 -0400 Subject: [PATCH 09/13] Additional fixes --- .../Materials/Layers/ML_CesiumVoxel.uasset | Bin 54245 -> 48808 bytes Shaders/Private/CesiumCylinder.usf | 39 +++++++++++++++--- Shaders/Private/CesiumVoxelTemplate.usf | 19 ++++----- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/Content/Materials/Layers/ML_CesiumVoxel.uasset b/Content/Materials/Layers/ML_CesiumVoxel.uasset index 92786610c1571a104a215194a9ef725044e082cc..81344ad19f6d157ca1549d9442ce64852ca672d0 100644 GIT binary patch delta 10389 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 BfUN)k delta 13061 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 ap{Nc@h8IO0Z1ncZ5s}M2?}kc_^#3nKoZQg> diff --git a/Shaders/Private/CesiumCylinder.usf b/Shaders/Private/CesiumCylinder.usf index 8b2350b7c..5f6a364b4 100644 --- a/Shaders/Private/CesiumCylinder.usf +++ b/Shaders/Private/CesiumCylinder.usf @@ -173,15 +173,34 @@ struct Cylinder RayIntersections FirstResult = (RayIntersections) 0; RayIntersections SecondResult = (RayIntersections) 0; IntersectFlippedWedge(R, AngleBounds, FirstResult, SecondResult); - setShapeIntersections(Intersections, ListState, 2, FirstResult); - setShapeIntersections(Intersections, ListState, 3, SecondResult); + switch (ListState.Length) + { + case 4: // outer cylinder + inner radius + setShapeIntersections(Intersections, ListState, 2, FirstResult); + setShapeIntersections(Intersections, ListState, 3, SecondResult); + break; + case 2: // outer cylinder + default: + setShapeIntersections(Intersections, ListState, 1, FirstResult); + setShapeIntersections(Intersections, ListState, 2, SecondResult); + break; + } ListState.Length += 4; } else if (AngleRangeFlag == ANGLE_OVER_HALF) { // The shape's angle range is over half, so we intersect a NEGATIVE shape that is under half. RayIntersections WedgeResult = IntersectRegularWedge(R, AngleBounds); - setShapeIntersections(Intersections, ListState, 2, WedgeResult); + switch (ListState.Length) + { + case 4: // outer cylinder + inner radius + setShapeIntersections(Intersections, ListState, 2, WedgeResult); + break; + case 2: // outer cylinder + default: + setShapeIntersections(Intersections, ListState, 1, WedgeResult); + break; + } ListState.Length += 2; } else if (AngleRangeFlag == ANGLE_EQUAL_ZERO) @@ -189,8 +208,18 @@ struct Cylinder RayIntersections FirstResult = (RayIntersections) 0; RayIntersections SecondResult = (RayIntersections) 0; IntersectHalfPlane(R, AngleBounds.x, FirstResult, SecondResult); - setShapeIntersections(Intersections, ListState, 2, FirstResult); - setShapeIntersections(Intersections, ListState, 3, SecondResult); + switch (ListState.Length) + { + case 4: // outer cylinder + inner radius + setShapeIntersections(Intersections, ListState, 2, FirstResult); + setShapeIntersections(Intersections, ListState, 3, SecondResult); + break; + case 2: // outer cylinder + default: + setShapeIntersections(Intersections, ListState, 1, FirstResult); + setShapeIntersections(Intersections, ListState, 2, SecondResult); + break; + } ListState.Length += 4; } } diff --git a/Shaders/Private/CesiumVoxelTemplate.usf b/Shaders/Private/CesiumVoxelTemplate.usf index 805a520c7..4d6fa0646 100644 --- a/Shaders/Private/CesiumVoxelTemplate.usf +++ b/Shaders/Private/CesiumVoxelTemplate.usf @@ -38,6 +38,8 @@ struct VoxelMegatextures { %s int ShapeConstant; + bool WrapCylinderUV; + uint3 TileCount; // Number of tiles in the texture, in three dimensions. // NOTE: Unlike VoxelOctree, these dimensions are specified with respect to the voxel attributes @@ -72,18 +74,13 @@ struct VoxelMegatextures { // Since glTFs are y-up (and 3D Tiles is z-up), the data must be accessed to reflect the transforms // from a Y-up to Z-up frame of reference, plus the Cesium -> Unreal transform as well. - // VoxelCoords = float3(VoxelCoords.x, (DataDimensions.y - 1u) - VoxelCoords.z, VoxelCoords.y); LocalUV = float3(LocalUV.x, clamp(1.0 - LocalUV.z, 0.0, 1.0), LocalUV.y); } - else if (ShapeConstant == CYLINDER) + else if (ShapeConstant == CYLINDER && WrapCylinderUV && Sample.Coords.w == 0) { - // For cylinders, the start of the angular bounds has to be adjusted for full cylinders - // (i.e., the root tile only). - // TODO: This assumes that the root tile is a whole cylinder, but the root tile may be - // a partial cylinder. + // For cylinders, the start of the angular bounds has to be adjusted for full cylinders + // (Root tile only). LocalUV = float3(LocalUV.x, frac(LocalUV.z + 0.5), LocalUV.y); - //float Angle = (Sample.Coords.w == 0) ? (VoxelCoords.y + (float)DataDimensions.z / 2.0) % DataDimensions.z : VoxelCoords.y; - //VoxelCoords = float3(VoxelCoords.x, VoxelCoords.z, Angle); } float3 VoxelCoords = floor(LocalUV * float3(GridDimensions)); @@ -166,9 +163,11 @@ Octree.SetNodeData(OctreeData); Octree.GridDimensions = GridDimensions; // Initialize data textures -VoxelMegatextures DataTextures; -DataTextures.ShapeConstant = ShapeConstant; +VoxelMegatexturesDataTextures = (VoxelMegatextures) 0; +DataTextures.ShapeConstant = +ShapeConstant; DataTextures.TileCount = TileCount; +DataTextures.WrapCylinderUV = (ShapeConstant == CYLINDER) ? (Octree.GridShape.CylinderShape.AngleRangeFlag == 0) : false; // Account for y-up -> z-up conventions for certain shapes. switch (ShapeConstant) { From dafcd5a27490c5fae265164f24e7e6f03486b4d6 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 5 Aug 2025 12:09:10 -0400 Subject: [PATCH 10/13] Fix metadata access for cylinder voxels [skip ci] --- .../Materials/Instances/MI_CesiumVoxel.uasset | Bin 11858 -> 12077 bytes .../Materials/Layers/ML_CesiumVoxel.uasset | Bin 48808 -> 48841 bytes Shaders/Private/CesiumCylinder.usf | 7 +++---- Shaders/Private/CesiumShape.usf | 18 +++++++++++------- Shaders/Private/CesiumVoxelTemplate.usf | 13 ++++++------- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Content/Materials/Instances/MI_CesiumVoxel.uasset b/Content/Materials/Instances/MI_CesiumVoxel.uasset index a36598ac242c9e920b33633d5981fb2699401418..d1587888a8527ee491884d81e488997f44343b20 100644 GIT binary patch delta 1234 zcmcZzR3f9*u3=Afd!CHucffY#i zi7_w;0_kT!)%zqE7(jBDfb=aOj#Fk}$nscg*~2a1zDgi!LvRoKhJQL=cO3qR*=F$la?nfy>(YVreJ zmdW2mZNw%(L>WM#tHKW97^uRPq>I^zO@t}|nGyy{@<0%x16Oig%tmqwL=>DMoS^Jg zpfo!XZNh%ZXY=Lo`%Ha4JwAbr4VDl=0;CuR1=@fswxQ7 zC9dkpm@_$1RR&Co$ilqu3ssM-&k3yWnCeQfZ5}{!r&<(a)no@%@yXii@uEdo^b~^h zNPz;F!48YG1K7yt>iJ+Jl_y`&;hgLuC@^`lh8JT6(D4S0C7by*C7Hm|8UnX(=Zb1? z^LK91UaW1;v*_gHL~TjYd@SY`fUOk)#R4{K?ZMVw*LG$sn=Gv}5pH=ClvSpC2LM!8 BE2RJc delta 918 zcmZ1*cPVDV3Kjts28PK3{1!qC3@i-&3=9k(85tOufV73+#BbVx34#m^tU%soQ3eJe zlYv{9f#I<@0|QVI!*?M4ABdZk7#M0A`QjgKo$R&$$h41goaVOwbiVFLWR-PavO>D* zX6d|a*@D)AOJb00w|`m`5EI&&dDEn|5FU>1qm~}Kmb8`z{7AtQ9RR3q7-ulVEg{3)RBA+II7nX<1e+0_6g5*&|+E7G3 z0M+zPo-HEH+XOZB8&ISXEYb`W`2rNF1BoL97EJ!oUrrRoEe%P>8ghxPw?HR2rl^3>5J|P^k^py-wUgvIimpP7+Q~ z_IX_(W3qsb_+&W=2eDot2V^}55c2{t2ngzd4A|T!!NSeTECCeQJY6}Hkuhg7qpAgC z?qoeZWibJuD96`@rx+>>R{BhT=~2&oy5Br-a=xa-I!+-!}e$?az z>f)0{)zlbMCflgV0Le%-6`;C0wKEVSC+DfF0$EekqZo@OPf(SZ%&rk9ng!O+z<_L0 zCdeedc94u6OnUNsjY&{rR_k(3j?;`{EC4&^qNW66&gNH|l1vb3h{HXimZV&B@pE6l w`z~L?ubrH(K>tdJro&8w(jaGNfGraNg)G9dew{SNyvg5mCc>?30<-=D0OhRQcmMzZ diff --git a/Content/Materials/Layers/ML_CesiumVoxel.uasset b/Content/Materials/Layers/ML_CesiumVoxel.uasset index 81344ad19f6d157ca1549d9442ce64852ca672d0..9619eb7f7044248ba7009558138b876d7bda86be 100644 GIT binary patch delta 8126 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 P^YpI`066IM3kUxX7(&{x delta 8008 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 HUODz(MeD?Z diff --git a/Shaders/Private/CesiumCylinder.usf b/Shaders/Private/CesiumCylinder.usf index 5f6a364b4..481aaa9d2 100644 --- a/Shaders/Private/CesiumCylinder.usf +++ b/Shaders/Private/CesiumCylinder.usf @@ -31,8 +31,8 @@ struct Cylinder void Initialize(float3 InMinBounds, float3 InMaxBounds, float4 PackedData0, float4 PackedData1, float4 PackedData2, float4 PackedData3) { - MinBounds = InMinBounds; // radius, angle, height - MaxBounds = InMaxBounds; // radius, angle, height + MinBounds = InMinBounds; // normalized radius, angle, height + MaxBounds = InMaxBounds; // normalized radius, angle, height // Flags are packed in CesiumVoxelRendererComponent.cpp RadiusHasMinimumBound = bool(PackedData0.x); @@ -156,8 +156,7 @@ struct Cylinder setSurfaceIntersection(Intersections, ListState, 1, InnerResult.Entry, false, true); // negative, entering setSurfaceIntersection(Intersections, ListState, 2, InnerResult.Exit, false, false); // negative, exiting setSurfaceIntersection(Intersections, ListState, 3, OuterResult.Exit, true, false); // positive, exiting - ListState.Length += 2; - + ListState.Length += 2; } else if (RadiusHasMinimumBound) { diff --git a/Shaders/Private/CesiumShape.usf b/Shaders/Private/CesiumShape.usf index e41a8f5bd..a01bf9a46 100644 --- a/Shaders/Private/CesiumShape.usf +++ b/Shaders/Private/CesiumShape.usf @@ -25,21 +25,25 @@ struct Shape void Initialize(in int InShapeConstant, float3 InMinBounds, float3 InMaxBounds, float4 PackedData0, float4 PackedData1, float4 PackedData2, float4 PackedData3, float4 PackedData4, float4 PackedData5) { ShapeConstant = InShapeConstant; - + BoxShape = (Box) 0; + CylinderShape = (Cylinder) 0; + RegionShape = (EllipsoidRegion) 0; + [branch] switch (ShapeConstant) { - case BOX: - // Initialize with default unit box bounds. - BoxShape.MinBounds = -1; - BoxShape.MaxBounds = 1; - break; case CYLINDER: CylinderShape.Initialize(InMinBounds, InMaxBounds, PackedData0, PackedData1, PackedData2, PackedData3); break; case ELLIPSOID: RegionShape.Initialize(InMinBounds, InMaxBounds, PackedData0, PackedData1, PackedData2, PackedData3, PackedData4, PackedData5); break; + case BOX: + default: + // Initialize with default unit box bounds. + BoxShape.MinBounds = -1; + BoxShape.MaxBounds = 1; + break; } } @@ -47,7 +51,7 @@ struct Shape * Tests whether the input ray (Unit Space) intersects the shape. */ RayIntersections Intersect(in Ray R, out float4 Intersections[INTERSECTIONS_LENGTH]) - { + { [branch] switch (ShapeConstant) { diff --git a/Shaders/Private/CesiumVoxelTemplate.usf b/Shaders/Private/CesiumVoxelTemplate.usf index 4d6fa0646..3c6d2e841 100644 --- a/Shaders/Private/CesiumVoxelTemplate.usf +++ b/Shaders/Private/CesiumVoxelTemplate.usf @@ -76,11 +76,12 @@ struct VoxelMegatextures // from a Y-up to Z-up frame of reference, plus the Cesium -> Unreal transform as well. LocalUV = float3(LocalUV.x, clamp(1.0 - LocalUV.z, 0.0, 1.0), LocalUV.y); } - else if (ShapeConstant == CYLINDER && WrapCylinderUV && Sample.Coords.w == 0) + else if (ShapeConstant == CYLINDER) { // For cylinders, the start of the angular bounds has to be adjusted for full cylinders // (Root tile only). - LocalUV = float3(LocalUV.x, frac(LocalUV.z + 0.5), LocalUV.y); + float adjustedAngle = WrapCylinderUV && Sample.Coords.w == 0 ? frac(LocalUV.y + 0.5) : LocalUV.y; + LocalUV = float3(LocalUV.x, LocalUV.z, adjustedAngle); } float3 VoxelCoords = floor(LocalUV * float3(GridDimensions)); @@ -163,9 +164,8 @@ Octree.SetNodeData(OctreeData); Octree.GridDimensions = GridDimensions; // Initialize data textures -VoxelMegatexturesDataTextures = (VoxelMegatextures) 0; -DataTextures.ShapeConstant = -ShapeConstant; +VoxelMegatextures DataTextures; +DataTextures.ShapeConstant = ShapeConstant; DataTextures.TileCount = TileCount; DataTextures.WrapCylinderUV = (ShapeConstant == CYLINDER) ? (Octree.GridShape.CylinderShape.AngleRangeFlag == 0) : false; @@ -250,8 +250,7 @@ for (int step = 0; step < STEP_COUNT_MAX; step++) { } } - PositionUV = R.Origin + CurrentT * R. -Direction; + PositionUV = R.Origin + CurrentT * R.Direction; PositionShapeUVSpace = Octree.GridShape.ConvertUVToShapeUVSpace(PositionUV, JacobianT); Octree.ResumeTraversal(PositionShapeUVSpace, Traversal, Sample); NextIntersection = Octree.GetNextVoxelIntersection(Sample, RawDirection, Intersections, JacobianT, CurrentT); From 3f51d371ef6d863e9f54f4a64404fc70bc3db39a Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 5 Aug 2025 17:09:36 -0400 Subject: [PATCH 11/13] Don't iterate over empty loop --- Shaders/Private/CesiumRayIntersectionList.usf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Shaders/Private/CesiumRayIntersectionList.usf b/Shaders/Private/CesiumRayIntersectionList.usf index 320ba5f9e..472d959e7 100644 --- a/Shaders/Private/CesiumRayIntersectionList.usf +++ b/Shaders/Private/CesiumRayIntersectionList.usf @@ -124,6 +124,12 @@ struct IntersectionListState continue; } + if (i >= Length) + { + Index = INTERSECTIONS_LENGTH; + break; + } + Index = i + 1; surfaceIntersection = Data[i]; From b1d0e419caf16235de32ebc09fd6a289a6113840 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 5 Aug 2025 17:33:08 -0400 Subject: [PATCH 12/13] Fix some things from self-review --- Content/ScreenSpacePawn.uasset | Bin 419109 -> 0 bytes Shaders/Private/CesiumEllipsoidRegion.usf | 133 ---------------------- Shaders/Private/CesiumVoxelTemplate.usf | 3 +- Source/CesiumRuntime/Private/VecMath.h | 1 - 4 files changed, 1 insertion(+), 136 deletions(-) delete mode 100644 Content/ScreenSpacePawn.uasset diff --git a/Content/ScreenSpacePawn.uasset b/Content/ScreenSpacePawn.uasset deleted file mode 100644 index 45d3f8bb26f9813f3cd442ce99a7def89ec870c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 419109 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 0.0) == (dot(R.Origin.xy, intersection.Normal.xy) > 0.0); - if (!hitFront) - { - return NewRayIntersections(intersection, farSide); - } - else - { - return NewRayIntersections(Multiply(farSide, -1), intersection); - } - } - - /** - * Intersects a "flipped" wedge formed by the negative space of the specified angle - * bounds and returns up to four intersections. The "flipped" wedge is the union of - * two half-spaces defined at the given angles and represents a *negative* volume - * of over > 180 degrees. - */ - void IntersectFlippedWedge(in Ray R, in float2 AngleBounds, out RayIntersections FirstIntersection, out RayIntersections SecondIntersection) - { - FirstIntersection = IntersectHalfSpace(R, AngleBounds.x, false); - SecondIntersection = IntersectHalfSpace(R, AngleBounds.y, true); - } - - /** - * Intersects the wedge formed by the negative space of the min/max longitude, where - * maxAngle > minAngle + pi. The wedge is represented by two planes at such angles. - * There is an opposite "shadow wedge", i.e. the wedge formed at the *opposite* side - * of the planes' intersection, that must be specially handled. - */ - RayIntersections IntersectRegularWedge(in Ray R, in float2 AngleBounds) - { - // Normals will point toward the "outside" (into the negative space) - Intersection intersect1 = IntersectLongitudePlane(R, AngleBounds.x, false); - Intersection intersect2 = IntersectLongitudePlane(R, AngleBounds.y, true); - - // Note: the intersections could be in the "shadow" wedge, beyond the tip of - // the actual wedge. - Intersection first = intersect1; - Intersection last = intersect2; - if (first.t > last.t) - { - first = intersect2; - last = intersect1; - } - - bool firstIntersectionAheadOfRay = first.t >= 0.0; - bool startedInsideFirst = dot(R.Origin.xy, first.Normal.xy) < 0.0; - bool isExitingFromInside = firstIntersectionAheadOfRay == startedInsideFirst; - bool lastIntersectionAheadOfRay = last.t > 0.0; - bool startedOutsideLast = dot(R.Origin.xy, last.Normal.xy) >= 0.0; - bool isEnteringFromOutside = lastIntersectionAheadOfRay == startedOutsideLast; - - Intersection farSide = NewIntersection(INF_HIT, normalize(R.Direction)); - Intersection miss = NewIntersection(NO_HIT, normalize(R.Direction)); - - if (isExitingFromInside && isEnteringFromOutside) - { - // Ray crosses both faces of negative wedge, exiting then entering the positive shape - return NewRayIntersections(first, last); - } - else if (!isExitingFromInside && isEnteringFromOutside) - { - // Ray starts inside wedge. last is in shadow wedge, and first is actually the entry - return NewRayIntersections(Multiply(farSide, -1), first); - } - else if (isExitingFromInside && !isEnteringFromOutside) - { - // First intersection was in the shadow wedge, so last is actually the exit - return NewRayIntersections(last, farSide); - } - else - { // !exitFromInside && !enterFromOutside - // Both intersections were in the shadow wedge - return NewRayIntersections(miss, miss); - } - } - - bool HitsPositiveHalfPlane(in Ray R, in Intersection InputIntersection, in bool positiveNormal) - { - float normalSign = positiveNormal ? 1.0 : -1.0; - float2 planeDirection = float2(InputIntersection.Normal.y, -InputIntersection.Normal.x) * normalSign; - float2 hit = R.Origin.xy + InputIntersection.t * R.Direction.xy; - return dot(hit, planeDirection) > 0.0; - } - - void IntersectHalfPlane(in Ray R, in float Angle, out RayIntersections FirstIntersection, out RayIntersections SecondIntersection) - { - Intersection intersection = IntersectLongitudePlane(R, Angle, true); - Intersection farSide = NewIntersection(INF_HIT, normalize(R.Direction)); - - if (HitsPositiveHalfPlane(R, intersection, true)) - { - FirstIntersection.Entry = Multiply(farSide, -1); - FirstIntersection.Exit = NewIntersection(intersection.t, float3(-1.0 * intersection.Normal.xy, 0.0)); - SecondIntersection.Entry = intersection; - SecondIntersection.Exit = farSide; - } - else - { - Intersection miss = NewIntersection(NO_HIT, normalize(R.Direction)); - FirstIntersection.Entry = Multiply(farSide, -1); - FirstIntersection.Exit = farSide; - SecondIntersection.Entry = miss; - SecondIntersection.Exit = miss; - } - } - /** * Given a circular quadric cone around the z-axis, with apex at the origin, * find the parametric distance(s) along a ray where that ray intersects diff --git a/Shaders/Private/CesiumVoxelTemplate.usf b/Shaders/Private/CesiumVoxelTemplate.usf index 3c6d2e841..230c4e902 100644 --- a/Shaders/Private/CesiumVoxelTemplate.usf +++ b/Shaders/Private/CesiumVoxelTemplate.usf @@ -78,8 +78,7 @@ struct VoxelMegatextures } else if (ShapeConstant == CYLINDER) { - // For cylinders, the start of the angular bounds has to be adjusted for full cylinders - // (Root tile only). + // The start of the angular bounds has to be adjusted for full cylinders (root tile only). float adjustedAngle = WrapCylinderUV && Sample.Coords.w == 0 ? frac(LocalUV.y + 0.5) : LocalUV.y; LocalUV = float3(LocalUV.x, LocalUV.z, adjustedAngle); } diff --git a/Source/CesiumRuntime/Private/VecMath.h b/Source/CesiumRuntime/Private/VecMath.h index 2634b9ace..ef92be90c 100644 --- a/Source/CesiumRuntime/Private/VecMath.h +++ b/Source/CesiumRuntime/Private/VecMath.h @@ -194,7 +194,6 @@ class VecMath { */ static FVector createVector(const glm::dvec3& v) noexcept; - /** * @brief Create an `FVector4` from the given `glm` 4D vector. * From 6c01f531cc57371e2080424b58ece844557d3af8 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 6 Aug 2025 10:36:37 -0400 Subject: [PATCH 13/13] Add missing header include --- Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.cpp | 2 ++ Source/CesiumRuntime/Private/VoxelMegatextures.cpp | 1 + Source/CesiumRuntime/Private/VoxelOctree.cpp | 1 + 3 files changed, 4 insertions(+) diff --git a/Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.cpp b/Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.cpp index df3e402a7..0d7e1bd80 100644 --- a/Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.cpp +++ b/Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.cpp @@ -1,6 +1,8 @@ #include "ExtensionImageAssetUnreal.h" #include "CesiumRuntime.h" #include "CesiumTextureUtility.h" +#include "RenderingThread.h" + #include #include diff --git a/Source/CesiumRuntime/Private/VoxelMegatextures.cpp b/Source/CesiumRuntime/Private/VoxelMegatextures.cpp index 5a0d758ed..5c643f621 100644 --- a/Source/CesiumRuntime/Private/VoxelMegatextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelMegatextures.cpp @@ -12,6 +12,7 @@ #include "EncodedFeaturesMetadata.h" #include "EncodedMetadataConversions.h" #include "Engine/VolumeTexture.h" +#include "RenderingThread.h" #include "Templates/UniquePtr.h" #include "UObject/Package.h" diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index 9f2e71091..d36b4128a 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -4,6 +4,7 @@ #include "CesiumLifetime.h" #include "CesiumRuntime.h" #include "CesiumTextureResource.h" +#include "RenderingThread.h" #include #include