diff --git a/Content/Materials/Instances/MI_CesiumVoxel.uasset b/Content/Materials/Instances/MI_CesiumVoxel.uasset index a36598ac2..d1587888a 100644 Binary files a/Content/Materials/Instances/MI_CesiumVoxel.uasset and b/Content/Materials/Instances/MI_CesiumVoxel.uasset differ diff --git a/Content/Materials/Layers/ML_CesiumVoxel.uasset b/Content/Materials/Layers/ML_CesiumVoxel.uasset index 28e011b07..9619eb7f7 100644 Binary files a/Content/Materials/Layers/ML_CesiumVoxel.uasset and b/Content/Materials/Layers/ML_CesiumVoxel.uasset differ diff --git a/Shaders/Private/CesiumCylinder.usf b/Shaders/Private/CesiumCylinder.usf new file mode 100644 index 000000000..481aaa9d2 --- /dev/null +++ b/Shaders/Private/CesiumCylinder.usf @@ -0,0 +1,303 @@ +#pragma once + +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumCylinder.usf: An implicit cylinder that may be intersected by a ray. +=============================================================================*/ + +#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 AngleMinAtDiscontinuity; + bool AngleMaxAtDiscontinuity; + bool AngleIsReversed; + + void Initialize(float3 InMinBounds, float3 InMaxBounds, float4 PackedData0, float4 PackedData1, float4 PackedData2, float4 PackedData3) + { + MinBounds = InMinBounds; // normalized radius, angle, height + MaxBounds = InMaxBounds; // normalized radius, angle, height + + // Flags are packed in CesiumVoxelRendererComponent.cpp + RadiusHasMinimumBound = bool(PackedData0.x); + RadiusIsFlat = bool(PackedData0.y); + + AngleRangeFlag = round(PackedData1.x); + AngleMinAtDiscontinuity = bool(PackedData1.y); + AngleMaxAtDiscontinuity = bool(PackedData1.z); + AngleIsReversed = bool(PackedData1.w); + + AngleUVExtents = PackedData2.xyz; + + RadiusUVScale = PackedData3.x; + RadiusUVOffset = PackedData3.y; + AngleUVScale = PackedData3.z; + AngleUVOffset = PackedData3.w; + } + + /** + * 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)); + + if (zDirection < 0.0) + { + // Ray entered from the top plane down. + 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 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; + } + + 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 + ListState.Length += 2; + } + else if (RadiusHasMinimumBound) + { + RayIntersections InnerResult = IntersectInfiniteCylinder(R, MinBounds.x, false); + setShapeIntersections(Intersections, ListState, 1, InnerResult); + ListState.Length += 2; + } + + float2 AngleBounds = float2(MinBounds.y, MaxBounds.y); + if (AngleRangeFlag == ANGLE_UNDER_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); + 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); + 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) + { + RayIntersections FirstResult = (RayIntersections) 0; + RayIntersections SecondResult = (RayIntersections) 0; + IntersectHalfPlane(R, AngleBounds.x, FirstResult, 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; + } + } + + /** + * Scales the input UV coordinates from [0, 1] to their values in UV Shape Space. + */ + float3 ScaleUVToShapeUVSpace(in float3 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); + } + + /** + * 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. + // 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; + + 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 (AngleMinAtDiscontinuity) + { + angleUV = angleUV > AngleUVExtents.z ? AngleUVExtents.x : angleUV; + } + else if (AngleMaxAtDiscontinuity) + { + angleUV = angleUV < AngleUVExtents.z ? AngleUVExtents.y : angleUV; + } + + angleUV = angleUV * AngleUVScale + AngleUVOffset; + } + + return float3(radiusUV, angleUV, height); + } +}; diff --git a/Shaders/Private/CesiumEllipsoidRegion.usf b/Shaders/Private/CesiumEllipsoidRegion.usf index 3b1bc2ea0..5122b2c5f 100644 --- a/Shaders/Private/CesiumEllipsoidRegion.usf +++ b/Shaders/Private/CesiumEllipsoidRegion.usf @@ -7,6 +7,7 @@ =============================================================================*/ #include "CesiumRayIntersectionList.usf" +#include "CesiumLongitudeWedge.usf" struct EllipsoidRegion { @@ -103,139 +104,6 @@ struct EllipsoidRegion return result; } - /** - * 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; - } - } - /** * 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 @@ -460,12 +328,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 +622,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/Shaders/Private/CesiumRayIntersectionList.usf b/Shaders/Private/CesiumRayIntersectionList.usf index 3954f02bc..6ddebc3eb 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 @@ -123,7 +123,7 @@ struct IntersectionListState { continue; } - + if (i >= Length) { Index = INTERSECTIONS_LENGTH; diff --git a/Shaders/Private/CesiumShape.usf b/Shaders/Private/CesiumShape.usf index 49998e2ab..a01bf9a46 100644 --- a/Shaders/Private/CesiumShape.usf +++ b/Shaders/Private/CesiumShape.usf @@ -6,14 +6,16 @@ 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 { int ShapeConstant; Box BoxShape; + Cylinder CylinderShape; EllipsoidRegion RegionShape; IntersectionListState ListState; @@ -23,18 +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; + 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; } } @@ -42,13 +51,16 @@ 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) { case BOX: BoxShape.Intersect(R, Intersections, ListState); break; + case CYLINDER: + CylinderShape.Intersect(R, Intersections, ListState); + break; case ELLIPSOID: RegionShape.Intersect(R, Intersections, ListState); break; @@ -59,14 +71,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) @@ -97,6 +107,8 @@ struct Shape [branch] switch (ShapeConstant) { + case CYLINDER: + return CylinderShape.ScaleUVToShapeUVSpace(UV); case ELLIPSOID: return RegionShape.ScaleUVToShapeUVSpace(UV); case BOX: @@ -111,10 +123,13 @@ struct Shape */ float3 ConvertUVToShapeUVSpace(in float3 UVPosition, out float3x3 JacobianT) { + [branch] switch (ShapeConstant) { 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..230c4e902 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 @@ -74,7 +76,13 @@ 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) + { + // 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); + } + float3 VoxelCoords = floor(LocalUV * float3(GridDimensions)); // Account for padding VoxelCoords = clamp(VoxelCoords + float3(PaddingBefore), 0, float3(DataDimensions - 1u)); @@ -131,7 +139,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. @@ -158,10 +166,12 @@ Octree.GridDimensions = GridDimensions; VoxelMegatextures DataTextures; 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) { case BOX: + case CYLINDER: DataTextures.GridDimensions = round(GridDimensions.xzy); DataTextures.PaddingBefore = round(PaddingBefore.xzy); DataTextures.PaddingAfter = round(PaddingAfter.xzy); @@ -225,15 +235,14 @@ 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; - } 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; diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 240a3d2a4..f04bbf9c2 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 @@ -83,6 +85,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; } @@ -134,7 +139,7 @@ void setVoxelBoxProperties( * defined in. This determines the math for the ray-intersection tested against * that value in the voxel shader. */ -enum CartographicAngleDescription : int8 { +enum AngleDescription : int8 { None = 0, Zero = 1, UnderHalf = 2, @@ -142,28 +147,269 @@ enum CartographicAngleDescription : int8 { OverHalf = 4 }; -CartographicAngleDescription interpretLongitudeRange(double value) { +AngleDescription interpretCylinderRange(double value) { + const double angleEpsilon = CesiumUtility::Math::Epsilon10; + + if (value >= CesiumUtility::Math::OnePi - angleEpsilon && + value < CesiumUtility::Math::TwoPi - angleEpsilon) { + // angle range >= PI + return AngleDescription::OverHalf; + } + if (value > angleEpsilon && + value < CesiumUtility::Math::OnePi - angleEpsilon) { + // angle range < PI + return AngleDescription::UnderHalf; + } + if (value <= angleEpsilon) { + // angle range ~= 0 + return AngleDescription::Zero; + } + + return AngleDescription::None; +} + +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)); + + // 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 normalizedRadiusRange = 1.0 - normalizedMinimumRadius; + bool hasNonzeroMinimumRadius = normalizedMinimumRadius > 0.0; + bool hasFlatRadius = radialBounds.x == radialBounds.y; + + if (hasNonzeroMinimumRadius && normalizedRadiusRange > 0.0) { + radiusUVScale = 1.0 / normalizedRadiusRange; + radiusUVOffset = -normalizedMinimumRadius / normalizedRadiusRange; + } + + radiusFlags.X = hasNonzeroMinimumRadius; + radiusFlags.Y = hasFlatRadius; + } + + // 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; + 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 + { + AngleDescription angleRangeIndicator = interpretCylinderRange(angleRange); + + // Refers to the discontinuity at angle -pi / pi. + const double discontinuityEpsilon = + CesiumUtility::Math::Epsilon3; // 0.001 radians = 0.05729578 degrees + bool angleMinimumAtDiscontinuity = CesiumUtility::Math::equalsEpsilon( + angularBounds.x, + -CesiumUtility::Math::OnePi, + discontinuityEpsilon); + bool angleMaximumAtDiscontinuity = CesiumUtility::Math::equalsEpsilon( + angularBounds.y, + CesiumUtility::Math::OnePi, + discontinuityEpsilon); + + angleFlags.X = angleRangeIndicator; + angleFlags.Y = angleMinimumAtDiscontinuity; + angleFlags.Z = angleMaximumAtDiscontinuity; + angleFlags.W = isAngleReversed; + + // Compute the extents of the angle range in UV Shape Space. + double minimumAngleUV = + (angularBounds.x - defaultMinimumBounds.Y) / defaultAngleRange; + double maximumAngleUV = + (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); + + angleUVExtents = + FVector(minimumAngleUV, maximumAngleUV, angleRangeUVZeroMid); + + const double angleEpsilon = CesiumUtility::Math::Epsilon10; + if (angleRange > angleEpsilon) { + angleUVScale = defaultAngleRange / 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(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.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 = 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 + // 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), + VecMath::createVector4(glm::row(localToUnit, 0))); + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape TransformToUnit Row 1"), + EMaterialParameterAssociation::LayerParameter, + 0), + VecMath::createVector4(glm::row(localToUnit, 1))); + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape TransformToUnit Row 2"), + EMaterialParameterAssociation::LayerParameter, + 0), + VecMath::createVector4(glm::row(localToUnit, 2))); +} + +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 @@ -171,18 +417,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) { @@ -264,7 +510,7 @@ void setVoxelEllipsoidProperties( defaultMaximumBounds.X, discontinuityEpsilon); - CartographicAngleDescription longitudeRangeIndicator = + AngleDescription longitudeRangeIndicator = interpretLongitudeRange(longitudeRange); longitudeFlags.X = longitudeRangeIndicator; @@ -298,9 +544,9 @@ void setVoxelEllipsoidProperties( } // Latitude - CartographicAngleDescription latitudeMinValueFlag = + AngleDescription latitudeMinValueFlag = interpretLatitudeValue(minimumLatitude); - CartographicAngleDescription latitudeMaxValueFlag = + AngleDescription latitudeMaxValueFlag = interpretLatitudeValue(maximumLatitude); double latitudeUVScale = 1; double latitudeUVOffset = 0; @@ -525,6 +771,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); 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/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..ef92be90c 100644 --- a/Source/CesiumRuntime/Private/VecMath.h +++ b/Source/CesiumRuntime/Private/VecMath.h @@ -194,6 +194,15 @@ 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. * 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