From 6b44d96c49ffd8b0c68b42b7e25077ffb9f84c4b Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 6 Mar 2025 14:32:20 -0500 Subject: [PATCH 01/34] Add metadata component and shader --- Content/Materials/CesiumVoxelTemplate.hlsl | 1008 +++++++++++++++++ .../Private/CesiumVoxelMetadataComponent.cpp | 948 ++++++++++++++++ .../Private/EncodedFeaturesMetadata.cpp | 27 + .../Private/EncodedFeaturesMetadata.h | 3 + .../CesiumFeaturesMetadataDescription.h | 55 + .../Public/CesiumVoxelMetadataComponent.h | 162 +++ 6 files changed, 2203 insertions(+) create mode 100644 Content/Materials/CesiumVoxelTemplate.hlsl create mode 100644 Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp create mode 100644 Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h diff --git a/Content/Materials/CesiumVoxelTemplate.hlsl b/Content/Materials/CesiumVoxelTemplate.hlsl new file mode 100644 index 000000000..b4a46eb67 --- /dev/null +++ b/Content/Materials/CesiumVoxelTemplate.hlsl @@ -0,0 +1,1008 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumVoxelTemplate.hlsl: Template for creating custom shaders to style voxel data. +=============================================================================*/ + +/*======================= + BEGIN CUSTOM SHADER +=========================*/ + +struct CustomShaderProperties +{ +%s +}; + +struct CustomShader +{ +%s + + float4 Shade(CustomShaderProperties Properties) + { +%s + } +}; + +/*======================= + END CUSTOM SHADER +=========================*/ + +/*=========================== + BEGIN RAY + INTERSECTION UTILITY +=============================*/ + +#define CZM_INFINITY 5906376272000.0 // Distance from the Sun to Pluto in meters. +#define NO_HIT -CZM_INFINITY +#define INF_HIT (CZM_INFINITY * 0.5) + +#define CZM_PI_OVER_TWO 1.5707963267948966 +#define CZM_PI 3.141592653589793 +#define CZM_TWO_PI 6.283185307179586 + +struct Ray +{ + float3 Origin; + float3 Direction; +}; + +struct Intersection +{ + float t; + float3 Normal; +}; + +// Represents where a ray enters and leaves a volume. +struct RayIntersections +{ + Intersection Entry; + Intersection Exit; +}; + +struct RayIntersectionUtility +{ + float MaxComponent(float3 v) + { + return max(max(v.x, v.y), v.z); + } + + float MinComponent(float3 v) + { + return min(min(v.x, v.y), v.z); + } + + bool3 Equal(float3 v1, float3 v2) + { + return bool3(v1.x == v2.x, v1.y == v2.y, v1.z == v2.z); + } + + Intersection NewMissedIntersection() + { + Intersection result = (Intersection) 0; + result.t = NO_HIT; + return result; + } + + Intersection NewIntersection(float t, float3 Normal) + { + Intersection result = (Intersection) 0; + result.t = t; + result.Normal = Normal; + return result; + } + + RayIntersections NewRayIntersections(Intersection Entry, Intersection Exit) + { + RayIntersections result = (RayIntersections) 0; + result.Entry = Entry; + result.Exit = Exit; + return result; + } + + Intersection Min(in Intersection A, in Intersection B) + { + if (A.t <= B.t) + { + return A; + } + return B; + } + + Intersection Max(in Intersection A, in Intersection B) + { + if (A.t >= B.t) + { + return A; + } + return B; + } + + bool IsInRange(in float3 v, in float3 min, in float3 max) + { + bool3 inRange = (clamp(v, min, max) == v); + return inRange.x && inRange.y && inRange.z; + } + + /** + * Construct an integer value (little endian) from two normalized uint8 values. + */ + int ConstructInt(in float2 value) + { + return int(value.x * 255.0) + (256 * int(value.y * 255.0)); + } + + Intersection Multiply(in Intersection intersection, in float scalar) + { + return NewIntersection(intersection.t * scalar, intersection.Normal * scalar); + } + + /* + * Resolves two intersection ranges of the ray by computing their overlap. + * For example: + * A |---------| + * B |-------------| + * Ray: O =========================> + * Output: |---| + */ + RayIntersections ResolveIntersections(in RayIntersections A, in RayIntersections B) + { + bool missed = (A.Entry.t == NO_HIT) || (B.Entry.t == NO_HIT) || + (A.Exit.t < B.Entry.t) || (A.Entry.t > B.Exit.t); + + if (missed) + { + Intersection miss = NewMissedIntersection(); + return NewRayIntersections(miss, miss); + } + + Intersection entry = Max(A.Entry, B.Entry); + Intersection exit = Min(A.Exit, B.Exit); + + return NewRayIntersections(entry, exit); + } +}; + +// 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 +#define INTERSECTIONS_LENGTH SHAPE_INTERSECTIONS * 2 + +// HLSL does not like array struct members, so the data has to be stored at the top-level. +// The size is also hardcoded because dynamically sized arrays are not allowed. +float4 miss = float4(0, 0, 0, NO_HIT); +float4 IntersectionList[INTERSECTIONS_LENGTH] = +{ + miss, miss, miss, miss, miss, miss, miss, + miss, miss, miss, miss, miss, miss, miss, +}; + +struct IntersectionListState +{ + // Don't access these member variables directly - call the functions instead. + // Store an array of encoded ray-surface intersections. (See EncodeIntersection). + + int Length; // Set based on ShapeConstant + int Index; // Used to emulate dynamic indexing. + + // The variables below relate to the shapes that the intersection is inside (counting when it is + // on the surface itself). e.g., given a hollow ellipsoid volume, + // count = 1 on the outer ellipsoid, 2 on the inner ellipsoid. + int SurroundingShapeCount; + bool IsInsidePositiveShape; // will be true as long as it is inside any positive shape. + + /** + * Intersections are encoded as float4s: + * - .xyz for the surface normal at the intersection point + * - .w for the T value + * The normal's scale encodes the shape intersection type: + * length(intersection.xyz) = 1: positive shape entry + * length(intersection.xyz) = 2: positive shape exit + * length(intersection.xyz) = 3: negative shape entry + * length(intersection.xyz) = 4: negative shape exit + * + * When the voxel volume is hollow, the "positive" shape is the original volume. + * The "negative" shape is subtracted from the positive shape. + */ + float4 EncodeIntersection(in Intersection Input, bool IsPositive, bool IsEntering) + { + float scale = float(!IsPositive) * 2.0 + float(!IsEntering) + 1.0; + return float4(Input.Normal * scale, Input.t); + } + + /** + * Sort the intersections from min T to max T with bubble sort. Also prepares for iteration + * over the intersections. + * + * Note: If this sorting function changes, some of the intersection tests may need to be updated. + * Search for "Sort()" to find those areas. + */ + void Sort(inout float4 Data[INTERSECTIONS_LENGTH]) + { + const int sortPasses = INTERSECTIONS_LENGTH - 1; + for (int n = sortPasses; n > 0; --n) + { + // Skip to n = Length - 1 + if (n >= Length) + { + continue; + } + + for (int i = 0; i < sortPasses; ++i) + { + // The loop should be: for (i = 0; i < n; ++i) {...} but since loops with + // non-constant conditions are not allowed, this breaks early instead. + if (i >= n) + { + break; + } + + float4 first = Data[i + 0]; + float4 second = Data[i + 1]; + + bool inOrder = first.w <= second.w; + Data[i + 0] = inOrder ? first : second; + Data[i + 1] = inOrder ? second : first; + } + } + + // Prepare initial state for GetNextIntersections() + Index = 0; + SurroundingShapeCount = 0; + IsInsidePositiveShape = false; + } + + RayIntersections GetFirstIntersections(in float4 Data[INTERSECTIONS_LENGTH]) + { + RayIntersections result = (RayIntersections) 0; + result.Entry.t = Data[0].w; + result.Entry.Normal = normalize(Data[0].xyz); + result.Exit.t = Data[1].w; + result.Exit.Normal = normalize(Data[1].xyz); + + return result; + } + + /** + * Gets the intersection at the current value of Index, while managing the state of the ray's + * trajectory with respect to the intersected shapes. + */ + RayIntersections GetNextIntersections(in float4 Data[INTERSECTIONS_LENGTH]) + { + RayIntersections result = (RayIntersections) 0; + result.Entry.t = NO_HIT; + result.Exit.t = NO_HIT; + + if (Index >= Length) + { + return result; + } + + float4 surfaceIntersection = float4(0, 0, 0, NO_HIT); + + for (int i = 0; i < INTERSECTIONS_LENGTH; ++i) + { + // The loop should be: for (i = index; i < loopCount; ++i) {...} but it's not possible + // to loop with non-constant condition. Instead, continue until i = index. + if (i < Index) + { + continue; + } + + Index = i + 1; + + surfaceIntersection = Data[i]; + // Maps from [1-4] -> [0-3] (see EncodeIntersection for the types) + int intersectionType = int(length(surfaceIntersection.xyz) - 0.5); + bool isCurrentShapePositive = intersectionType < 2; + bool isEnteringShape = (intersectionType % 2) == 0; + + SurroundingShapeCount += isEnteringShape ? +1 : -1; + IsInsidePositiveShape = isCurrentShapePositive ? isEnteringShape : IsInsidePositiveShape; + + // True if entering positive shape or exiting negative shape + if (IsInsidePositiveShape && isEnteringShape == isCurrentShapePositive) + { + result.Entry.t = surfaceIntersection.w; + result.Entry.Normal = normalize(surfaceIntersection.xyz); + } + + // True if exiting the outermost positive shape + bool isExitingOutermostShape = !isEnteringShape && isCurrentShapePositive && SurroundingShapeCount == 0; + // True if entering negative shape while being inside a positive one + bool isEnteringNegativeFromPositive = isEnteringShape && !isCurrentShapePositive && SurroundingShapeCount == 2 && IsInsidePositiveShape; + + if (isExitingOutermostShape || isEnteringNegativeFromPositive) + { + result.Exit.t = surfaceIntersection.w; + result.Exit.Normal = normalize(surfaceIntersection.xyz); + // Entry and exit have been found, so the loop can stop + if (isExitingOutermostShape) + { + // After exiting the outermost positive shape, there is nothing left to intersect. Jump to the end. + Index = INTERSECTIONS_LENGTH; + } + break; + } + // Otherwise, keep searching for the correct exit. + } + + return result; + } +}; + +// Use defines instead of real functions to get array access with a non-constant index. + +/** +* Encodes and stores a single intersection. +*/ +#define setSurfaceIntersection(/*inout float4[]*/ list, /*in IntersectionListState*/ state, /*int*/ index, /*Intersection*/ intersection, /*bool*/ isPositive, /*bool*/ isEntering) (list)[(index)] = (state).EncodeIntersection((intersection), (isPositive), (isEntering)) + +/** +* Encodes and stores the given shape intersections, i.e., the intersections where a ray enters and exits a volume. +*/ +#define setShapeIntersections(/*inout float4[]*/ list, /*in IntersectionListState*/ state, /*int*/ pairIndex, /*RayIntersection*/ intersections) (list)[(pairIndex) * 2 + 0] = (state).EncodeIntersection((intersections).Entry, (pairIndex) == 0, true); (list)[(pairIndex) * 2 + 1] = (state).EncodeIntersection((intersections).Exit, (pairIndex) == 0, false) + +/*=========================== + END RAY + INTERSECTION UTILITY +=============================*/ + +/*=========================== + BEGIN SHAPE UTILITY +=============================*/ + +#define BOX 1 +#define CYLINDER 2 +#define ELLIPSOID 3 + +struct ShapeUtility +{ + RayIntersectionUtility Utils; + IntersectionListState ListState; + + int ShapeConstant; + + float3 MinBounds; + float3 MaxBounds; + + /** + * Converts a position from Unit Shape Space coordinates to UV Space. + * [-1, -1] => [0, 1] + */ + float3 UnitToUV(float3 UnitPosition) + { + return 0.5 * UnitPosition + 0.5; + } + + /** + * Converts a position from UV Space coordinates to Unit Shape Space. + * [0, 1] => [-1, -1] + */ + float3 UVToUnit(float3 UVPosition) + { + return 2.0 * UVPosition - 1.0; + } + + /** + * Interpret the input bounds (Local Space) according to the voxel grid shape. + */ + void Initialize(in int InShapeConstant, in float3 InMinBounds, in float3 InMaxBounds, in float4 PackedData0, in float4 PackedData1, in float4 PackedData2, in float4 PackedData3, in float4 PackedData4, in float4 PackedData5) + { + ShapeConstant = InShapeConstant; + ListState = (IntersectionListState) 0; + + if (ShapeConstant == BOX) + { + // Default unit box bounds. + MinBounds = float3(-1, -1, -1); + MaxBounds = float3(1, 1, 1); + } + } + + /** + * Tests whether the input ray (Unit Space) intersects the box. Outputs the intersections in Unit Space. + */ + void IntersectBox(in Ray R, out float4 Intersections[INTERSECTIONS_LENGTH]) + { + ListState.Length = 2; + + // Consider the box as the intersection of the space between 3 pairs of parallel planes. + + // Compute the distance along the ray to each plane. + float3 t0 = (MinBounds - R.Origin) / R.Direction; + float3 t1 = (MaxBounds - R.Origin) / R.Direction; + + // Identify candidate entries/exits based on distance from ray position. + float3 entries = min(t0, t1); + float3 exits = max(t0, t1); + + // The actual intersection points are the furthest entry and the closest exit. + // Do not allow intersections to go behind the shape (negative t). + RayIntersections result = (RayIntersections) 0; + float entryT = max(Utils.MaxComponent(entries), 0); + float exitT = max(Utils.MinComponent(exits), 0); + + if (entryT > exitT) + { + result.Entry.t = NO_HIT; + result.Exit.t = NO_HIT; + setShapeIntersections(Intersections, ListState, 0, result); + return; + } + + // Compute normals + float3 directions = sign(R.Direction); + bool3 isLastEntry = bool3(Utils.Equal(entries, float3(entryT, entryT, entryT))); + result.Entry.Normal = -1.0 * float3(isLastEntry) * directions; + result.Entry.t = entryT; + + bool3 isFirstExit = bool3(Utils.Equal(exits, float3(exitT, exitT, exitT))); + result.Exit.Normal = float3(isFirstExit) * directions; + result.Exit.t = exitT; + + setShapeIntersections(Intersections, ListState, 0, result); + } + + /** + * Tests whether the input ray (Unit Space) intersects the shape. + */ + RayIntersections + IntersectShape(in Ray R, out float4 Intersections[INTERSECTIONS_LENGTH]) + { + [branch] + switch (ShapeConstant) + { + case BOX: + IntersectBox(R, Intersections); + break; + default: + return Utils.NewRayIntersections(Utils.NewIntersection(NO_HIT, 0), Utils.NewIntersection(NO_HIT, 0)); + } + + RayIntersections result = ListState.GetFirstIntersections(Intersections); + return result; + } + + /** + * Scales the input UV coordinates from [0, 1] to their values in UV Shape Space. + */ + float3 ScaleUVToShapeUVSpace(in float3 UV) + { + return UV; + } + + /** + * Converts the input position (vanilla UV Space) to its Shape UV Space relative to the + * box geometry. Also outputs the Jacobian transpose for future use. + */ + float3 ConvertUVToShapeUVSpaceBox(in float3 PositionUV, out float3x3 JacobianT) + { + // For Box, Cartesian UV space = UV shape space, so we can use PositionUV as-is. + // The Jacobian is the identity matrix, except that a step of 1 only spans half the shape + // space [-1, 1], so the identity is scaled. + JacobianT = float3x3(0.5f, 0, 0, 0, 0.5f, 0, 0, 0, 0.5f); + return PositionUV; + } + + /** + * Converts the input position (vanilla UV Space) to its Shape UV Space relative to the + * voxel grid geometry. Also outputs the Jacobian transpose for future use. + */ + float3 ConvertUVToShapeUVSpace(in float3 UVPosition, out float3x3 JacobianT) + { + switch (ShapeConstant) + { + case BOX: + return ConvertUVToShapeUVSpaceBox(UVPosition, JacobianT); + default: + // Default return + JacobianT = float3x3(1, 0, 0, 0, 1, 0, 0, 0, 1); + return UVPosition; + } + } +}; + +/*=========================== + END SHAPE UTILITY +=============================*/ + +/*=========================== + BEGIN OCTREE TRAVERSAL UTILITY +=============================*/ + +// An octree node is encoded by 9 texels. +// The first texel contains the index of the node's parent. +// The other texels contain the indices of the node's children. +#define TEXELS_PER_NODE 9 + +#define OCTREE_MAX_LEVELS 32 // Hardcoded because HLSL doesn't like variable length loops + +#define OCTREE_FLAG_EMPTY 0 +#define OCTREE_FLAG_LEAF 1 +#define OCTREE_FLAG_INTERNAL 2 + +#define MINIMUM_STEP_SCALAR (0.02) +#define SHIFT_EPSILON (0.001) + +struct Node +{ + int Flag; + // "Data" is interpreted differently depending on the flag of the node. + // For internal nodes, this refers to the index of the node in the octree texture, + // where its children data are stored. + // For leaf nodes, this represents the index in the megatexture where the node's + // actual metadata is stored. + int Data; + // Only applicable to leaf nodes. + int LevelDifference; +}; + +/* + * Representation of an in-progress traversal of an octree. Points to a node at the given coords, + * and keeps track of the parent in case the traversal needs to be reversed. + * + * NOTE the difference between "coordinates" and "index". + * Octree coordinates are given as int4, representing the X, Y, Z, of the node in its tree level (W). + * These are not to be confused with the index of a node within the actual texture. + */ +struct OctreeTraversal +{ + int4 Coords; // W = Octree Level (starting from 0); XYZ = Index of node within that level + int Index; +}; + +struct TileSample +{ + int Index; // Index of the sample in the voxel data texture. + int4 Coords; + float3 LocalUV; +}; + +struct Octree +{ + Texture2D NodeData; + uint TextureWidth; + uint TextureHeight; + uint TilesPerRow; + uint3 GridDimensions; + + ShapeUtility ShapeUtils; + + /** + * Sets the octree texture and computes related variables. + */ + void SetNodeData(in Texture2D DataTexture) + { + NodeData = DataTexture; + NodeData.GetDimensions(TextureWidth, TextureHeight); + TilesPerRow = TextureWidth / TEXELS_PER_NODE; + } + + /** + * Given an octree-relative UV position, converts it to the corresponding position + * within the UV-space of the *tile* at the specified coordinates. + */ + float3 GetTileUV(in float3 PositionUV, in int4 Coords) + { + // Get the size of the tile at the given level. + float tileSize = float(1u << Coords.w); + return PositionUV * tileSize - float3(Coords.xyz); + } + + /** + * Given an octree-relative UV position, checks whether it is inside the tile + * specified by the octree coords. Assumes the position is always inside the + * root tile of the tileset. + */ + bool IsInsideTile(in float3 PositionUV, in int4 OctreeCoords) + { + float3 tileUV = GetTileUV(PositionUV, OctreeCoords); + bool isInside = ShapeUtils.Utils.IsInRange(tileUV, 0, 1); + return isInside || OctreeCoords.w == 0; + } + + /** + * Gets the size of a voxel cell within the given octree level. + */ + float3 GetVoxelSizeAtLevel(in int Level) + { + float3 sampleCount = float(1u << Level) * float3(GridDimensions); + float3 voxelSizeUV = 1.0 / sampleCount; + return ShapeUtils.ScaleUVToShapeUVSpace(voxelSizeUV); + } + + TileSample GetSampleFromNode(in Node Node, in float4 OctreeCoords) + { + TileSample result = (TileSample) 0; + result.Index = (Node.Flag != OCTREE_FLAG_EMPTY) + ? Node.Data + : -1; + float denominator = float(1u << Node.LevelDifference); + result.Coords = int4(OctreeCoords.xyz / denominator, OctreeCoords.w - Node.LevelDifference); + return result; + } + + Node GetNodeFromTexture(in int2 TextureIndex) + { + float4 nodeData = NodeData.Load(int3(TextureIndex.x, TextureIndex.y, 0)); + Node node = (Node) 0; + node.Flag = int(nodeData.x * 255.0); + node.LevelDifference = int(nodeData.y * 255.0); + node.Data = ShapeUtils.Utils.ConstructInt(nodeData.zw); + return node; + } + + /** + * Given the index of a node, converts it to texture indices, which are needed to actually + * retrieve the node data from the octree texture. + */ + int2 GetTextureIndexOfNode(in int NodeIndex) + { + int2 result; + result.x = (NodeIndex % TilesPerRow) * TEXELS_PER_NODE; + result.y = NodeIndex / TilesPerRow; + return result; + } + + Node GetChildNode(in int NodeIndex, in int3 ChildCoord) + { + int childIndex = ChildCoord.z * 4 + ChildCoord.y * 2 + ChildCoord.x; + int2 textureIndex = GetTextureIndexOfNode(NodeIndex); + textureIndex.x += 1 + childIndex; + return GetNodeFromTexture(textureIndex); + } + + Node GetParentNode(in int NodeIndex) + { + return GetNodeFromTexture(GetTextureIndexOfNode(NodeIndex)); + } + + /** + * Given a UV position [0, 1] within the voxel grid, traverse to the leaf containing it. + * Outputs the node's corresponding data texture index and octree coordinates. + */ + Node TraverseToLeaf(in float3 PositionUV, inout OctreeTraversal Traversal) + { + // Get the size of the node at the current level + float sizeAtLevel = exp2(-1.0 * float(Traversal.Coords.w)); + float3 start = float3(Traversal.Coords.xyz) * sizeAtLevel; + float3 end = start + sizeAtLevel; + + Node child; + + for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) + { + // Find the octree child that contains the given position. + // Example: the point (0.75, 0.25, 0.75) belongs to the child at coords (1, 0, 1) + float3 center = 0.5 * (start + end); + int3 childCoord = step(center, PositionUV); + + // Get the octree coordinates for the next level down. + Traversal.Coords = int4(Traversal.Coords.xyz * 2 + childCoord, Traversal.Coords.w + 1); + + child = GetChildNode(Traversal.Index, childCoord); + + if (child.Flag != OCTREE_FLAG_INTERNAL) + { + // Found leaf - stop traversing + break; + } + + // Keep going! + start = lerp(start, center, childCoord); + end = lerp(center, end, childCoord); + Traversal.Index = child.Data; + } + + return child; + } + + void BeginTraversal(in float3 PositionUV, out OctreeTraversal Traversal, out TileSample Sample) + { + Traversal = (OctreeTraversal) 0; + + // Start from root + Node currentNode = GetNodeFromTexture(int2(0, 0)); + if (currentNode.Flag == OCTREE_FLAG_INTERNAL) + { + currentNode = TraverseToLeaf(PositionUV, Traversal); + } + + Sample = GetSampleFromNode(currentNode, Traversal.Coords); + Sample.LocalUV = clamp(GetTileUV(PositionUV, Sample.Coords), 0.0, 1.0); + } + + void ResumeTraversal(in float3 PositionUV, inout OctreeTraversal Traversal, inout TileSample Sample) + { + if (IsInsideTile(PositionUV, Traversal.Coords)) + { + // Continue to sample the same tile, marching to a different voxel. + Sample.LocalUV = clamp(GetTileUV(PositionUV, Sample.Coords), 0.0, 1.0); + return; + } + + // Otherwise, go up tree until we find a parent tile containing the position. + for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) + { + Traversal.Coords.w -= 1; // Up one level + Traversal.Coords.xyz /= 2; // Get coordinates of the parent tile. + + if (IsInsideTile(PositionUV, Traversal.Coords)) + { + break; + } + Node parent = GetParentNode(Traversal.Index); + Traversal.Index = parent.Data; + } + + // Go down tree + Node node = TraverseToLeaf(PositionUV, Traversal); + Sample = GetSampleFromNode(node, Traversal.Coords); + Sample.LocalUV = clamp(GetTileUV(PositionUV, Sample.Coords), 0.0, 1.0); + } + + /** + * Given UV coordinates within a tile, and the voxel dimensions along a ray + * passing through the coordinates, find the intersections where the ray enters + * and exits the voxel cell. + * + * Outputs the distance to the points and the surface normals in UV Shape Space. + */ + RayIntersections GetVoxelIntersections(in float3 TileUV, in float3 VoxelSizeAlongRay) + { + float3 voxelCoord = TileUV * float3(GridDimensions); + float3 directions = sign(VoxelSizeAlongRay); + float3 positiveDirections = max(directions, 0.0); + float3 entryCoord = lerp(ceil(voxelCoord), floor(voxelCoord), positiveDirections); + float3 exitCoord = entryCoord + directions; + + RayIntersections Intersections; + + float3 distanceFromEntry = -abs((entryCoord - voxelCoord) * VoxelSizeAlongRay); + float lastEntry = ShapeUtils.Utils.MaxComponent(distanceFromEntry); + bool3 isLastEntry = ShapeUtils.Utils.Equal(distanceFromEntry, float3(lastEntry, lastEntry, lastEntry)); + Intersections.Entry.Normal = -1.0 * float3(isLastEntry) * directions; + Intersections.Entry.t = lastEntry; + + float3 distanceToExit = abs((exitCoord - voxelCoord) * VoxelSizeAlongRay); + float firstExit = ShapeUtils.Utils.MinComponent(distanceToExit); + bool3 isFirstExit = ShapeUtils.Utils.Equal(distanceToExit, float3(firstExit, firstExit, firstExit)); + Intersections.Exit.Normal = float3(isFirstExit) * directions; + Intersections.Exit.t = firstExit; + + return Intersections; + } + + /** + * Computes the next intersection in the octree by stepping to the next voxel cell + * within the shape along the ray. + * + * Outputs the distance relative to the initial intersection, as opposed to the distance from the + * origin of the ray. + */ + Intersection GetNextVoxelIntersection(in TileSample Sample, in float3 Direction, in RayIntersections ShapeIntersections, in float3x3 JacobianT, in float CurrentT) + { + // The Jacobian is computed in a space where the shape spans [-1, 1]. + // But the ray is marched in a space where the shape fills [0, 1]. + // So we need to scale the Jacobian by 2. + float3 gradient = 2.0 * mul(Direction, JacobianT); + float3 voxelSizeAlongRay = GetVoxelSizeAtLevel(Sample.Coords.w) / gradient; + RayIntersections voxelIntersections = GetVoxelIntersections(Sample.LocalUV, voxelSizeAlongRay); + + // Transform normal from UV Shape Space to Cartesian space. + float3 voxelNormal = normalize(mul(JacobianT, voxelIntersections.Entry.Normal)); + // Then, compare with the original shape intersection to choose the appropriate normal. + Intersection voxelEntry = ShapeUtils.Utils.NewIntersection(CurrentT + voxelIntersections.Entry.t, voxelNormal); + Intersection entry = ShapeUtils.Utils.Max(ShapeIntersections.Entry, voxelEntry); + + float fixedStep = ShapeUtils.Utils.MinComponent(abs(voxelSizeAlongRay)); + float shift = fixedStep * SHIFT_EPSILON; + float dt = voxelIntersections.Exit.t + shift; + if ((CurrentT + dt) > ShapeIntersections.Exit.t) + { + // Stop at end of shape. + dt = ShapeIntersections.Exit.t - CurrentT + shift; + } + float stepSize = clamp(dt, fixedStep * MINIMUM_STEP_SCALAR, fixedStep + shift); + + return ShapeUtils.Utils.NewIntersection(stepSize, entry.Normal); + } +}; + +/*=========================== + END OCTREE TRAVERSAL UTILITY +=============================*/ + +/*=========================== + BEGIN VOXEL DATA TEXTURE UTILITY +=============================*/ + +struct VoxelDataTextures +{ + %s + int ShapeConstant; + uint3 TileCount; // Number of tiles in the texture, in three dimensions. + + // NOTE: Unlike Octree, these dimensions are specified with respect to the voxel attributes + // in the glTF model. For box and cylinder voxels, the YZ dimensions will be swapped. + uint3 GridDimensions; + uint3 PaddingBefore; + uint3 PaddingAfter; + + int3 TileIndexToCoords(in int Index) + { + if (TileCount.x == 0 || TileCount.y == 0 || TileCount.z == 0) + { + return 0; + } + int ZSlice = TileCount.x * TileCount.y; + int Z = Index / ZSlice; + int Y = (Index % ZSlice) / TileCount.x; + int X = Index % TileCount.x; + return int3(X, Y, Z) * (GridDimensions + PaddingBefore + PaddingAfter); + } + + CustomShaderProperties GetProperties(in TileSample Sample) + { + // Compute the tile location within the texture. + float3 TileCoords = TileIndexToCoords(Sample.Index); + + // Compute int coordinates of the voxel within the tile. + float3 LocalUV = Sample.LocalUV; + uint3 DataDimensions = GridDimensions + PaddingBefore + PaddingAfter; + + 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); + } + else if (ShapeConstant == CYLINDER) + { + + } + + 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 + + return Properties; + } +}; + +/*=========================== + END VOXEL DATA TEXTURE UTILITY +=============================*/ + +/*=========================== + MAIN FUNCTION BODY +=============================*/ + +#define STEP_COUNT_MAX 1000 +#define ALPHA_ACCUMULATION_MAX 0.98 // Must be > 0.0 and <= 1.0 + +Octree VoxelOctree; +VoxelOctree.ShapeUtils = (ShapeUtility) 0; +VoxelOctree.ShapeUtils.Initialize(ShapeConstant, ShapeMinBounds, ShapeMaxBounds, PackedData0, PackedData1, PackedData2, PackedData3, PackedData4, PackedData5); + +Ray R = (Ray) 0; +R.Origin = RayOrigin; +R.Direction = RayDirection; + +// Input ray is Unit Space. +// +// Ultimately, we want to traverse the voxel grid in a [0, 1] UV space to simplify much of +// the voxel octree math. This is simply referred to UV space, or "vanilla" UV space. +// However, this UV space won't always be a perfect voxel cube. It must conform to the specified +// shape of the voxel volume. Voxel cells may be curved (e.g., around a cylinder), or non-uniformly +// scaled. This shape-relative UV space is aptly referred to as Shape UV Space. +// +// Shape UV Space is DIFFERENT from the shape's Unit Space. Think of Unit Space as the perfect +// version of the grid geometry: a solid box, cylinder, or ellipsoid, within [-1, 1]. +// The actual voxel volume can be a subsection of that perfect unit space, e.g., a hollowed +// cylinder with radius [0.5, 1]. +// +// 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, +// 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. +// Spaces will be referred to as follows: +// +// Unit Space: Unit space of the grid geometry from [-1, 1]. A perfectly solid box, cylinder, or ellipsoid. +// Shape UV Space: Voxel space from [0, 1] conforming to the actual voxel volume. The volume could be a box, a part of a cylinder, or a region on an ellipsoid. +// (Vanilla) UV Space: Voxel space within an untransformed voxel octree. This can be envisioned as a simple cube spanning [0, 1] in three dimensions. + +RayIntersections Intersections = VoxelOctree.ShapeUtils.IntersectShape(R, IntersectionList); +if (Intersections.Entry.t == NO_HIT) { + return 0; +} + +// Intersections are returned in Unit Space. Transform to UV space [0, 1] for raymarching through octree. +R.Origin = VoxelOctree.ShapeUtils.UnitToUV(R.Origin); +R.Direction = R.Direction * 0.5; + +// Initialize octree +VoxelOctree.SetNodeData(OctreeData); +VoxelOctree.GridDimensions = GridDimensions; + +// Initialize data textures +VoxelDataTextures DataTextures; +DataTextures.ShapeConstant = ShapeConstant; +DataTextures.TileCount = TileCount; + +switch (ShapeConstant) { + case BOX: + DataTextures.GridDimensions = round(GridDimensions.xzy); + DataTextures.PaddingBefore = round(PaddingBefore.xzy); + DataTextures.PaddingAfter = round(PaddingAfter.xzy); + break; + default: + DataTextures.GridDimensions = round(GridDimensions); + DataTextures.PaddingBefore = round(PaddingBefore); + DataTextures.PaddingAfter = round(PaddingAfter); + break; +} + +%s + +float CurrentT = Intersections.Entry.t; +float EndT = Intersections.Exit.t; +float3 PositionUV = R.Origin + CurrentT * R.Direction; + +// The Jacobian is necessary to compute voxel intersections with respect to the grid's shape. +float3x3 JacobianT; +float3 PositionShapeUVSpace = VoxelOctree.ShapeUtils.ConvertUVToShapeUVSpace(PositionUV, JacobianT); + +float3 RawDirection = R.Direction; + +OctreeTraversal Traversal; +TileSample Sample; +VoxelOctree.BeginTraversal(PositionShapeUVSpace, Traversal, Sample); +Intersection NextIntersection = VoxelOctree.GetNextVoxelIntersection(Sample, RawDirection, Intersections, JacobianT, CurrentT); + +float4 AccumulatedResult = float4(0, 0, 0, 0); + +CustomShaderProperties Properties; +CustomShader CS; + +for (int step = 0; step < STEP_COUNT_MAX; step++) { + if (Sample.Index >= 0) { + Properties = DataTextures.GetProperties(Sample); + // TODO: expose additional properties? + float4 result = CS.Shade(Properties); + AccumulatedResult += result; + + // Stop traversing if the alpha has been fully saturated. + if (AccumulatedResult.w > ALPHA_ACCUMULATION_MAX) { + AccumulatedResult.w = ALPHA_ACCUMULATION_MAX; + break; + } + } + + if (NextIntersection.t <= 0.0) { + // Shape is infinitely thin. The ray may have hit the edge of a + // foreground voxel. Step ahead slightly to check for more voxels + NextIntersection.t = 0.00001; + } + + // Keep raymarching + CurrentT += NextIntersection.t; + if (CurrentT > EndT) { + break; + } + + PositionUV = R.Origin + CurrentT * R.Direction; + PositionShapeUVSpace = VoxelOctree.ShapeUtils.ConvertUVToShapeUVSpace(PositionUV, JacobianT); + VoxelOctree.ResumeTraversal(PositionShapeUVSpace, Traversal, Sample); + NextIntersection = VoxelOctree.GetNextVoxelIntersection(Sample, RawDirection, Intersections, JacobianT, CurrentT); +} + +// Convert the alpha from [0,ALPHA_ACCUMULATION_MAX] to [0,1] +AccumulatedResult.a /= ALPHA_ACCUMULATION_MAX; + +return AccumulatedResult; diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp new file mode 100644 index 000000000..e093609ce --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -0,0 +1,948 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "CesiumVoxelMetadataComponent.h" + +#include "Cesium3DTileset.h" +#include "CesiumGltfComponent.h" +#include "CesiumGltfPrimitiveComponent.h" +#include "CesiumModelMetadata.h" +#include "CesiumRuntime.h" +#include "CesiumVoxelRendererComponent.h" +#include "EncodedFeaturesMetadata.h" +#include "EncodedMetadataConversions.h" +#include "GenerateMaterialUtility.h" +#include "Misc/FileHelper.h" +#include "UnrealMetadataConversions.h" + +#include + +#if WITH_EDITOR +#include "AssetRegistry/AssetData.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "ComponentReregisterContext.h" +#include "Containers/LazyPrintf.h" +#include "Containers/Map.h" +#include "ContentBrowserModule.h" +#include "Factories/MaterialFunctionMaterialLayerFactory.h" +#include "IContentBrowserSingleton.h" +#include "IMaterialEditor.h" +#include "Interfaces/IPluginManager.h" +#include "Materials/Material.h" +#include "Materials/MaterialAttributeDefinitionMap.h" +#include "Materials/MaterialExpressionAppendVector.h" +#include "Materials/MaterialExpressionCustom.h" +#include "Materials/MaterialExpressionFunctionInput.h" +#include "Materials/MaterialExpressionFunctionOutput.h" +#include "Materials/MaterialExpressionIf.h" +#include "Materials/MaterialExpressionMaterialFunctionCall.h" +#include "Materials/MaterialExpressionReroute.h" +#include "Materials/MaterialExpressionScalarParameter.h" +#include "Materials/MaterialExpressionSetMaterialAttributes.h" +#include "Materials/MaterialExpressionTextureCoordinate.h" +#include "Materials/MaterialExpressionTextureObjectParameter.h" +#include "Materials/MaterialExpressionTextureProperty.h" +#include "Materials/MaterialExpressionVectorParameter.h" +#include "Misc/PackageName.h" +#include "Modules/ModuleManager.h" +#include "Subsystems/AssetEditorSubsystem.h" +#include "UObject/Package.h" + +#include + +#if ENGINE_VERSION_5_3_OR_HIGHER +#define GET_INPUTS_MEMBER GetInputsView +#else +#define GET_INPUTS_MEMBER GetInputs +#endif + +using namespace EncodedFeaturesMetadata; +using namespace GenerateMaterialUtility; + +UCesiumVoxelMetadataComponent::UCesiumVoxelMetadataComponent() + : UActorComponent() { + // Structure to hold one-time initialization + struct FConstructorStatics { + ConstructorHelpers::FObjectFinder DefaultVolumeTexture; + FConstructorStatics() + : DefaultVolumeTexture( + TEXT("/Engine/EngineResources/DefaultVolumeTexture")) {} + }; + static FConstructorStatics ConstructorStatics; + DefaultVolumeTexture = ConstructorStatics.DefaultVolumeTexture.Object; + +#if WITH_EDITOR + this->UpdateShaderPreview(); +#endif +} + +#if WITH_EDITOR +void UCesiumVoxelMetadataComponent::PostLoad() { + Super::PostLoad(); + this->UpdateShaderPreview(); +} + +void UCesiumVoxelMetadataComponent::PostEditChangeProperty( + FPropertyChangedEvent& PropertyChangedEvent) { + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (!PropertyChangedEvent.Property) { + return; + } + + FName PropName = PropertyChangedEvent.Property->GetFName(); + FString PropNameAsString = PropertyChangedEvent.Property->GetName(); + + if (!PropertyChangedEvent.Property) { + return; + } + + if (PropName == GET_MEMBER_NAME_CHECKED( + UCesiumVoxelMetadataComponent, + CustomShader) || + PropName == GET_MEMBER_NAME_CHECKED( + UCesiumVoxelMetadataComponent, + AdditionalFunctions) || + PropName == + GET_MEMBER_NAME_CHECKED(UCesiumVoxelMetadataComponent, Description)) { + this->UpdateShaderPreview(); + } +} + +void UCesiumVoxelMetadataComponent::PostEditChangeChainProperty( + FPropertyChangedChainEvent& PropertyChangedChainEvent) { + Super::PostEditChangeChainProperty(PropertyChangedChainEvent); + if (!PropertyChangedChainEvent.Property || + PropertyChangedChainEvent.PropertyChain.IsEmpty()) { + return; + } + this->UpdateShaderPreview(); +} + +namespace { +FCesiumMetadataValueType +GetValueTypeFromClassProperty(const Cesium3DTiles::ClassProperty& Property) { + FCesiumMetadataValueType ValueType; + ValueType.Type = (ECesiumMetadataType)CesiumGltf::convertStringToPropertyType( + Property.type); + ValueType.ComponentType = (ECesiumMetadataComponentType) + CesiumGltf::convertStringToPropertyComponentType( + Property.componentType.value_or("")); + ValueType.bIsArray = Property.array; + return ValueType; +} + +void AutoFillVoxelClassDescription( + FCesiumVoxelClassDescription& Description, + const std::string& VoxelClassID, + const Cesium3DTiles::Class& VoxelClass) { + + for (const auto& propertyIt : VoxelClass.properties) { + auto pExistingProperty = Description.Properties.FindByPredicate( + [&propertyName = propertyIt.first]( + const FCesiumPropertyAttributePropertyDescription& + existingProperty) { + return existingProperty.Name == propertyName.c_str(); + }); + + if (pExistingProperty) { + // We have already accounted for this property. + continue; + } + + FCesiumPropertyAttributePropertyDescription& property = + Description.Properties.Emplace_GetRef(); + property.Name = propertyIt.first.c_str(); + + property.PropertyDetails.SetValueType( + GetValueTypeFromClassProperty(propertyIt.second)); + property.PropertyDetails.ArraySize = propertyIt.second.count.value_or(0); + property.PropertyDetails.bIsNormalized = propertyIt.second.normalized; + + // These values are not actually validated until the material is generated. + property.PropertyDetails.bHasOffset = propertyIt.second.offset.has_value(); + property.PropertyDetails.bHasScale = propertyIt.second.scale.has_value(); + property.PropertyDetails.bHasNoDataValue = + propertyIt.second.noData.has_value(); + property.PropertyDetails.bHasDefaultValue = + propertyIt.second.defaultProperty.has_value(); + + property.EncodingDetails = CesiumMetadataPropertyDetailsToEncodingDetails( + property.PropertyDetails); + } +} +} // namespace + +void UCesiumVoxelMetadataComponent::AutoFill() { + const ACesium3DTileset* pOwner = this->GetOwner(); + const Cesium3DTilesSelection::Tileset* pTileset = + pOwner ? pOwner->GetTileset() : nullptr; + if (!pTileset) { + return; + } + + const Cesium3DTiles::ExtensionContent3dTilesContentVoxels* pVoxelExtension = + pTileset->getVoxelContentExtension(); + if (!pVoxelExtension) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Tileset %s does not contain voxel content, so CesiumVoxelMetadataComponent will have no effect."), + *pOwner->GetName()); + return; + } + + // TODO turn into helper? function + const Cesium3DTilesSelection::TilesetMetadata* pMetadata = + pTileset->getMetadata(); + if (!pMetadata || !pMetadata->schema) { + return; + } + + const std::string& voxelClassId = pVoxelExtension->classProperty; + if (pMetadata->schema->classes.find(voxelClassId) == + pMetadata->schema->classes.end()) { + return; + } + + Super::PreEditChange(NULL); + + AutoFillVoxelClassDescription( + this->Description, + voxelClassId, + pMetadata->schema->classes.at(voxelClassId)); + + Super::PostEditChange(); + + UpdateShaderPreview(); +} + +namespace { +struct MaterialResourceLibrary { + FString HlslShaderTemplate; + UMaterialFunctionMaterialLayer* MaterialLayerTemplate; + UVolumeTexture* DefaultVolumeTexture; + + MaterialResourceLibrary() { + static FString ContentDir = IPluginManager::Get() + .FindPlugin(TEXT("CesiumForUnreal")) + ->GetContentDir(); + FFileHelper::LoadFileToString( + HlslShaderTemplate, + *(ContentDir / "Materials/CesiumVoxelTemplate.hlsl")); + + MaterialLayerTemplate = LoadObjFromPath( + "/CesiumForUnreal/Materials/Layers/ML_CesiumVoxel"); + } + + bool isValid() const { + return !HlslShaderTemplate.IsEmpty() && MaterialLayerTemplate && + DefaultVolumeTexture; + } +}; + +/** + * Utility for filling CesiumVoxelTemplate.hlsl with necessary code / parameters + * for styling voxels in an Unreal material. + */ +struct CustomShaderBuilder { + FString DeclareShaderProperties; + FString SamplePropertiesFromTexture; + FString DeclareDataTextureVariables; + FString SetDataTextures; + + /** + * Declares the property in the CustomShaderProperties struct for use in the + * shader. + */ + void AddPropertyDeclaration( + const FString& PropertyName, + const FCesiumPropertyAttributePropertyDescription& Property) { + if (!DeclareShaderProperties.IsEmpty()) { + DeclareShaderProperties += "\n"; + } + + FString encodedHlslType = GetHlslTypeForEncodedType( + Property.EncodingDetails.Type, + Property.EncodingDetails.ComponentType); + FString normalizedHlslType = FString(); + + bool isNormalizedProperty = Property.PropertyDetails.bIsNormalized; + + if (isNormalizedProperty) { + normalizedHlslType = GetHlslTypeForEncodedType( + Property.EncodingDetails.Type, + ECesiumEncodedMetadataComponentType::Float); + FString rawPropertyName = PropertyName + MaterialPropertyRawSuffix; + + // If the property is normalized, the encoded type actually corresponds to + // the raw data values. A second line for the normalized value is added. + // e.g., "uint8 myProperty_RAW; float myProperty;" + // clang-format off + DeclareShaderProperties += "\t" + encodedHlslType + " " + rawPropertyName + + ";\n\t" + + normalizedHlslType + " " + PropertyName + ";"; + // clang-format on + } else { + // e.g., "float temperature;" + DeclareShaderProperties += + "\t" + encodedHlslType + " " + PropertyName + ";"; + } + + if (Property.PropertyDetails.bHasNoDataValue) { + // Expose "no data" value to the shader so the user can act on it. + // "No data" values are always given in the raw value type. + FString NoDataName = PropertyName + MaterialPropertyNoDataSuffix; + DeclareShaderProperties += + "\n\t" + encodedHlslType + " " + NoDataName + ";"; + } + + if (Property.PropertyDetails.bHasDefaultValue) { + // Expose default value to the shader so the user can act on it. + FString DefaultValueName = + PropertyName + MaterialPropertyDefaultValueSuffix; + DeclareShaderProperties += + "\n\t" + isNormalizedProperty ? normalizedHlslType : encodedHlslType; + DeclareShaderProperties += " " + DefaultValueName + ";"; + } + } + + /** + * Declares the texture parameter in the VoxelDataTextures struct for use in + * the shader. + */ + void AddDataTexture( + const FString& PropertyName, + const FString& TextureParameterName) { + if (!DeclareDataTextureVariables.IsEmpty()) { + DeclareDataTextureVariables += "\n\t"; + } + // e.g., "Texture3D temperature;" + DeclareDataTextureVariables += "Texture3D " + PropertyName + ";"; + + if (!SetDataTextures.IsEmpty()) { + SetDataTextures += "\n"; + } + // e.g., "DataTextures.temperature = temperature_DATA;" + SetDataTextures += + "DataTextures." + PropertyName + " = " + TextureParameterName + ";"; + } + + /** + * Adds code for correctly retrieving the property from the VoxelDataTextures. + * Also adds and applies any value transforms in the property. + */ + void AddPropertyRetrieval( + const FString& PropertyName, + const FCesiumPropertyAttributePropertyDescription& Property) { + if (!SamplePropertiesFromTexture.IsEmpty()) { + SamplePropertiesFromTexture += "\n\t\t"; + } + + FString encodedHlslType = GetHlslTypeForEncodedType( + Property.EncodingDetails.Type, + Property.EncodingDetails.ComponentType); + FString normalizedHlslType = GetHlslTypeForEncodedType( + Property.EncodingDetails.Type, + ECesiumEncodedMetadataComponentType::Float); + FString rawPropertyName = PropertyName + MaterialPropertyRawSuffix; + + FString swizzle = GetSwizzleForEncodedType(Property.EncodingDetails.Type); + bool isNormalizedProperty = Property.PropertyDetails.bIsNormalized; + if (isNormalizedProperty) { + SamplePropertiesFromTexture += "Properties." + rawPropertyName + " = " + + PropertyName + ".Load(int4(Coords, 0))" + + swizzle + ";"; + // Normalization can be hardcoded because only normalized uint8s are + // supported. + SamplePropertiesFromTexture += "\n\t\tProperties." + PropertyName + + " = (Properties." + rawPropertyName + + " / 255.0)"; + } else { + SamplePropertiesFromTexture += "Properties." + PropertyName + " = " + + PropertyName + ".Load(int4(Coords, 0))" + + swizzle; + } + + if (Property.PropertyDetails.bHasScale) { + FString ScaleName = PropertyName + MaterialPropertyScaleSuffix; + // Declare the value transforms underneath the corresponding data texture + // variable. e.g., float myProperty_SCALE; + DeclareDataTextureVariables += + "\n\t" + isNormalizedProperty ? normalizedHlslType : encodedHlslType; + DeclareDataTextureVariables += " " + ScaleName + ";"; + SetDataTextures += + "\nDataTextures." + ScaleName + " = " + ScaleName + ";"; + + // e.g., " * myProperty_SCALE" + SamplePropertiesFromTexture += " * " + ScaleName; + } + + if (Property.PropertyDetails.bHasOffset) { + FString OffsetName = PropertyName + MaterialPropertyOffsetSuffix; + DeclareDataTextureVariables += + "\n\t" + isNormalizedProperty ? normalizedHlslType : encodedHlslType; + DeclareDataTextureVariables += " " + OffsetName + ";"; + SetDataTextures += + "\nDataTextures." + OffsetName + " = " + OffsetName + ";"; + + // e.g., " + myProperty_OFFSET" + SamplePropertiesFromTexture += " + " + OffsetName; + } + + SamplePropertiesFromTexture += ";"; + + if (Property.PropertyDetails.bHasNoDataValue) { + FString NoDataName = PropertyName + MaterialPropertyNoDataSuffix; + DeclareDataTextureVariables += + "\n\t" + encodedHlslType + " " + NoDataName + ";"; + SetDataTextures += + "\nDataTextures." + NoDataName + " = " + NoDataName + ";"; + + SamplePropertiesFromTexture += + "\n\tProperties." + NoDataName + " = " + NoDataName + ";"; + } + + if (Property.PropertyDetails.bHasDefaultValue) { + FString DefaultValueName = + PropertyName + MaterialPropertyDefaultValueSuffix; + DeclareDataTextureVariables += + "\n\t" + isNormalizedProperty ? normalizedHlslType : encodedHlslType; + DeclareDataTextureVariables += " " + DefaultValueName + ";"; + SetDataTextures += + "\nDataTextures." + DefaultValueName + " = " + DefaultValueName + ";"; + + SamplePropertiesFromTexture += + "\n\tProperties." + DefaultValueName + " = " + DefaultValueName + ";"; + } + } + + /** + * Comprehensively adds the declaration for properties and data textures, as + * well as the code to correctly retrieve the property values from the data + * textures. + */ + void AddShaderProperty( + const FString& PropertyName, + const FString& TextureParameterName, + const FCesiumPropertyAttributePropertyDescription& Property) { + AddPropertyDeclaration(PropertyName, Property); + AddDataTexture(PropertyName, TextureParameterName); + AddPropertyRetrieval(PropertyName, Property); + } +}; +} // namespace + +/*static*/ const FString UCesiumVoxelMetadataComponent::ShaderPreviewTemplate = + "struct CustomShaderProperties {\n" + "%s" + "\n}\n\n" + "struct CustomShader {\n" + "%s\n\n" + "\tfloat4 Shade(CustomShaderProperties Properties) {\n" + "%s\n" + "\t}\n}"; +#endif + +void UCesiumVoxelMetadataComponent::UpdateShaderPreview() { + // Inspired by HLSLMaterialTranslator.cpp. Similar to MaterialTemplate.ush, + // CesiumVoxelTemplate.ush contains "%s" formatters that should be replaced + // with generated code. + FLazyPrintf LazyPrintf(*ShaderPreviewTemplate); + CustomShaderBuilder Builder; + + const TArray& Properties = + this->Description.Properties; + for (const FCesiumPropertyAttributePropertyDescription& Property : + Properties) { + if (!isSupportedPropertyAttributeProperty(Property.PropertyDetails)) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Property %s of type %s, component type %s is not supported for voxels and will not be added to the generated material."), + *Property.Name, + *MetadataTypeToString(Property.PropertyDetails.Type), + *MetadataComponentTypeToString( + Property.PropertyDetails.ComponentType)); + continue; + } + + FString PropertyName = createHlslSafeName(Property.Name); + Builder.AddPropertyDeclaration(PropertyName, Property); + } + + LazyPrintf.PushParam(*Builder.DeclareShaderProperties); + LazyPrintf.PushParam(*this->AdditionalFunctions); + LazyPrintf.PushParam(*this->CustomShader); + + this->CustomShaderPreview = LazyPrintf.GetResultString(); +} + +static MaterialNodeClassification +ClassifyNodes(UMaterialFunctionMaterialLayer* Layer) { + MaterialNodeClassification Classification; + for (const TObjectPtr& Node : + Layer->GetExpressionCollection().Expressions) { + // Check if this node is marked as autogenerated. + if (Node->Desc.StartsWith( + AutogeneratedMessage, + ESearchCase::Type::CaseSensitive)) { + Classification.AutoGeneratedNodes.Add(Node); + } + } + return Classification; +} + +static void ClearAutoGeneratedNodes( + UMaterialFunctionMaterialLayer* Layer, + MaterialGenerationState& MaterialState) { + MaterialNodeClassification Classification = ClassifyNodes(Layer); + // Remove auto-generated nodes. + for (UMaterialExpression* AutoGeneratedNode : + Classification.AutoGeneratedNodes) { + Layer->GetExpressionCollection().RemoveExpression(AutoGeneratedNode); + } +} + +/** + * @brief Generates the nodes necessary to apply property transforms to a + * metadata property. + */ +static void GenerateNodesForMetadataPropertyTransforms( + UMaterialFunctionMaterialLayer* Layer, + TArray& AutoGeneratedNodes, + const FCesiumMetadataPropertyDetails& PropertyDetails, + ECesiumEncodedMetadataType Type, + const FString& PropertyName, + int32& NodeX, + int32& NodeY, + UMaterialExpressionCustom* RaymarchNode) { + if (PropertyDetails.bHasScale) { + NodeY += Incr; + FString ParameterName = PropertyName + MaterialPropertyScaleSuffix; + UMaterialExpressionParameter* Parameter = + GenerateParameterNode(Layer, Type, ParameterName, NodeX, NodeY); + AutoGeneratedNodes.Add(Parameter); + + FCustomInput& ScaleInput = RaymarchNode->Inputs.Emplace_GetRef(); + ScaleInput.InputName = FName(ParameterName); + ScaleInput.Input.Expression = Parameter; + } + + if (PropertyDetails.bHasOffset) { + NodeY += Incr; + FString ParameterName = PropertyName + MaterialPropertyOffsetSuffix; + UMaterialExpressionParameter* Parameter = + GenerateParameterNode(Layer, Type, ParameterName, NodeX, NodeY); + AutoGeneratedNodes.Add(Parameter); + + FCustomInput& OffsetInput = RaymarchNode->Inputs.Emplace_GetRef(); + OffsetInput.InputName = FName(ParameterName); + OffsetInput.Input.Expression = Parameter; + } + + FString swizzle = GetSwizzleForEncodedType(Type); + + if (PropertyDetails.bHasNoDataValue) { + NodeY += Incr; + FString ParameterName = PropertyName + MaterialPropertyNoDataSuffix; + UMaterialExpressionParameter* Parameter = + GenerateParameterNode(Layer, Type, ParameterName, NodeX, NodeY); + AutoGeneratedNodes.Add(Parameter); + + FCustomInput& NoDataInput = RaymarchNode->Inputs.Emplace_GetRef(); + NoDataInput.InputName = FName(ParameterName); + NoDataInput.Input.Expression = Parameter; + } + + if (PropertyDetails.bHasDefaultValue) { + NodeY += Incr; + FString ParameterName = PropertyName + MaterialPropertyDefaultValueSuffix; + UMaterialExpressionParameter* Parameter = + GenerateParameterNode(Layer, Type, ParameterName, NodeX, NodeY); + + FCustomInput& DefaultInput = RaymarchNode->Inputs.Emplace_GetRef(); + DefaultInput.InputName = FName(ParameterName); + DefaultInput.Input.Expression = Parameter; + } +} + +static void GenerateMaterialNodes( + UCesiumVoxelMetadataComponent* Component, + MaterialGenerationState& MaterialState, + const MaterialResourceLibrary& ResourceLibrary) { + UMaterialFunctionMaterialLayer* pTargetLayer = Component->TargetMaterialLayer; + + int32 NodeX = 0; + int32 NodeY = 0; + int32 DataSectionX = 0; + int32 DataSectionY = 0; + + FMaterialExpressionCollection& SrcCollection = + ResourceLibrary.MaterialLayerTemplate->GetExpressionCollection(); + TMap SrcToDestMap; + MaterialState.AutoGeneratedNodes.Reserve(SrcCollection.Expressions.Num()); + + UMaterialExpressionCustom* RaymarchNode = nullptr; + UMaterialExpressionMaterialFunctionCall* BreakFloat4Node = nullptr; + + for (const UMaterialExpression* SrcExpression : SrcCollection.Expressions) { + // Ignore the standard input / output nodes; these do not need duplication. + const auto* InputMaterial = + Cast(SrcExpression); + const auto* SetAttributes = + Cast(SrcExpression); + const auto* OutputMaterial = + Cast(SrcExpression); + if (InputMaterial || SetAttributes || OutputMaterial) { + continue; + } + + // Much of the code below is derived from + // UMaterialExpression::CopyMaterialExpressions(). + UMaterialExpression* NewExpression = + Cast(StaticDuplicateObject( + SrcExpression, + pTargetLayer, + NAME_None, + RF_Transactional)); + + // Make sure we remove any references to materials or functions the nodes + // came from. + NewExpression->Material = nullptr; + NewExpression->Function = nullptr; + + SrcToDestMap.Add(SrcExpression, NewExpression); + + // Add to list of autogenerated nodes. + MaterialState.AutoGeneratedNodes.Add(NewExpression); + + // There can be only one default mesh paint texture. + UMaterialExpressionTextureBase* TextureSample = + Cast(NewExpression); + if (TextureSample) { + TextureSample->IsDefaultMeshpaintTexture = false; + } + + NewExpression->UpdateParameterGuid(true, true); + NewExpression->UpdateMaterialExpressionGuid(true, true); + + auto* CustomNode = Cast(NewExpression); + if (CustomNode && CustomNode->GetDescription() == "Voxel Raymarch") { + RaymarchNode = CustomNode; + continue; + } + + auto* MaterialFunctionNode = + Cast(NewExpression); + if (MaterialFunctionNode) { + BreakFloat4Node = MaterialFunctionNode; + continue; + } + + auto* VectorParameterNode = + Cast(NewExpression); + if (VectorParameterNode && + VectorParameterNode->ParameterName.ToString() == "Tile Count") { + DataSectionX = VectorParameterNode->MaterialExpressionEditorX; + DataSectionY = VectorParameterNode->MaterialExpressionEditorY; + } + } + + if (!RaymarchNode) { + UE_LOG( + LogCesium, + Error, + TEXT("Unable to generate material from ML_CesiumVoxels template.")) + return; + } + + // Fix up internal references. Iterate over the inputs of the new + // expressions, and for each input that refers to an expression that was + // duplicated, point the reference to that new expression. Otherwise, clear + // the input. + for (UMaterialExpression* NewExpression : MaterialState.AutoGeneratedNodes) { + const TArray& ExpressionInputs = + NewExpression->GetInputs(); + for (int32 ExpressionInputIndex = 0; + ExpressionInputIndex < ExpressionInputs.Num(); + ++ExpressionInputIndex) { + FExpressionInput* Input = ExpressionInputs[ExpressionInputIndex]; + UMaterialExpression* InputExpression = Input->Expression; + if (InputExpression) { + UMaterialExpression** NewInputExpression = + SrcToDestMap.Find(InputExpression); + if (NewInputExpression) { + check(*NewInputExpression); + Input->Expression = *NewInputExpression; + } else { + Input->Expression = nullptr; + } + } + } + } + + NodeX = DataSectionX; + NodeY = DataSectionY; + + // Inspired by HLSLMaterialTranslator.cpp. Similar to MaterialTemplate.ush, + // CesiumVoxelTemplate.hlsl contains "%s" formatters that will be replaced + // with generated code. + FLazyPrintf LazyPrintf(*ResourceLibrary.HlslShaderTemplate); + CustomShaderBuilder Builder; + + const TArray& Properties = + Component->Description.Properties; + RaymarchNode->Inputs.Reserve(RaymarchNode->Inputs.Num() + Properties.Num()); + for (const FCesiumPropertyAttributePropertyDescription& Property : + Properties) { + if (!isSupportedPropertyAttributeProperty(Property.PropertyDetails)) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Property %s of type %s, component type %s is not supported for voxels and will not be added to the generated material."), + *Property.Name, + *MetadataTypeToString(Property.PropertyDetails.Type), + *MetadataComponentTypeToString( + Property.PropertyDetails.ComponentType)); + continue; + } + + NodeY += Incr; + + FString PropertyName = createHlslSafeName(Property.Name); + // Example: "temperature_DATA" + FString PropertyDataName = PropertyName + MaterialPropertyDataSuffix; + + UMaterialExpressionTextureObjectParameter* PropertyData = + NewObject(pTargetLayer); + PropertyData->ParameterName = FName(PropertyName); + PropertyData->MaterialExpressionEditorX = NodeX; + PropertyData->MaterialExpressionEditorY = NodeY; + // Set the initial value to default volume texture to avoid compilation + // errors with the default 2D texture. + PropertyData->Texture = ResourceLibrary.DefaultVolumeTexture; + MaterialState.AutoGeneratedNodes.Add(PropertyData); + + FCustomInput& PropertyInput = RaymarchNode->Inputs.Emplace_GetRef(); + PropertyInput.InputName = FName(PropertyDataName); + PropertyInput.Input.Expression = PropertyData; + + GenerateNodesForMetadataPropertyTransforms( + pTargetLayer, + MaterialState.AutoGeneratedNodes, + Property.PropertyDetails, + Property.EncodingDetails.Type, + PropertyName, + NodeX, + NodeY, + RaymarchNode); + + Builder.AddShaderProperty(PropertyName, PropertyDataName, Property); + } + + LazyPrintf.PushParam(*Builder.DeclareShaderProperties); + LazyPrintf.PushParam(*Component->AdditionalFunctions); + LazyPrintf.PushParam(*Component->CustomShader); + LazyPrintf.PushParam(*Builder.DeclareDataTextureVariables); + LazyPrintf.PushParam(*Builder.SamplePropertiesFromTexture); + LazyPrintf.PushParam(*Builder.SetDataTextures); + + RaymarchNode->Code = LazyPrintf.GetResultString(); + + UMaterialExpressionFunctionInput* InputMaterial = nullptr; + for (const TObjectPtr& ExistingNode : + pTargetLayer->GetExpressionCollection().Expressions) { + UMaterialExpressionFunctionInput* ExistingInputMaterial = + Cast(ExistingNode); + if (ExistingInputMaterial) { + InputMaterial = ExistingInputMaterial; + break; + } + } + + NodeX = 0; + NodeY = 0; + + if (!InputMaterial) { + InputMaterial = NewObject(pTargetLayer); + InputMaterial->InputType = + EFunctionInputType::FunctionInput_MaterialAttributes; + InputMaterial->bUsePreviewValueAsDefault = true; + InputMaterial->MaterialExpressionEditorX = NodeX; + InputMaterial->MaterialExpressionEditorY = NodeY; + MaterialState.OneTimeGeneratedNodes.Add(InputMaterial); + } + + NodeX += 10 * Incr; + + UMaterialExpressionSetMaterialAttributes* SetMaterialAttributes = nullptr; + for (const TObjectPtr& ExistingNode : + pTargetLayer->GetExpressionCollection().Expressions) { + UMaterialExpressionSetMaterialAttributes* ExistingSetAttributes = + Cast(ExistingNode); + if (ExistingSetAttributes) { + SetMaterialAttributes = ExistingSetAttributes; + break; + } + } + + if (!SetMaterialAttributes) { + SetMaterialAttributes = + NewObject(pTargetLayer); + MaterialState.OneTimeGeneratedNodes.Add(SetMaterialAttributes); + } + + SetMaterialAttributes->Inputs.Reset(3); + SetMaterialAttributes->AttributeSetTypes.Reset(2); + + SetMaterialAttributes->Inputs.EmplaceAt(0, FExpressionInput()); + SetMaterialAttributes->Inputs[0].Expression = InputMaterial; + + SetMaterialAttributes->Inputs.EmplaceAt(1, FExpressionInput()); + SetMaterialAttributes->Inputs[1].Connect(0, RaymarchNode); + SetMaterialAttributes->Inputs[1].InputName = "Base Color"; + + SetMaterialAttributes->Inputs.EmplaceAt(2, FExpressionInput()); + SetMaterialAttributes->Inputs[2].Connect(3, BreakFloat4Node); + SetMaterialAttributes->Inputs[2].InputName = "Opacity"; + + // SetMaterialAttributes needs to manage an internal list of which attributes + // were selected. + const TArray& OrderedVisibleAttributes = + FMaterialAttributeDefinitionMap::GetOrderedVisibleAttributeList(); + for (const FGuid& AttributeID : OrderedVisibleAttributes) { + const FString& name = + FMaterialAttributeDefinitionMap::GetAttributeName(AttributeID); + if (name == "BaseColor") { + SetMaterialAttributes->AttributeSetTypes.EmplaceAt(0, AttributeID); + } else if (name == "Opacity") { + SetMaterialAttributes->AttributeSetTypes.EmplaceAt(1, AttributeID); + } + } + + SetMaterialAttributes->MaterialExpressionEditorX = NodeX; + SetMaterialAttributes->MaterialExpressionEditorY = NodeY; + + NodeX += 2 * Incr; + + UMaterialExpressionFunctionOutput* OutputMaterial = nullptr; + for (const TObjectPtr& ExistingNode : + pTargetLayer->GetExpressionCollection().Expressions) { + UMaterialExpressionFunctionOutput* ExistingOutputMaterial = + Cast(ExistingNode); + if (ExistingOutputMaterial) { + OutputMaterial = ExistingOutputMaterial; + break; + } + } + + if (!OutputMaterial) { + OutputMaterial = NewObject(pTargetLayer); + MaterialState.OneTimeGeneratedNodes.Add(OutputMaterial); + } + + OutputMaterial->MaterialExpressionEditorX = NodeX; + OutputMaterial->MaterialExpressionEditorY = NodeY; + OutputMaterial->A = FMaterialAttributesInput(); + OutputMaterial->A.Expression = SetMaterialAttributes; +} + +void UCesiumVoxelMetadataComponent::GenerateMaterial() { + ACesium3DTileset* pTileset = Cast(this->GetOwner()); + if (!pTileset) { + return; + } + + MaterialResourceLibrary ResourceLibrary; + ResourceLibrary.DefaultVolumeTexture = this->DefaultVolumeTexture; + if (!ResourceLibrary.isValid()) { + UE_LOG( + LogCesium, + Error, + TEXT( + "Can't find the material or shader templates necessary to generate voxel material. Aborting.")); + return; + } + + if (this->TargetMaterialLayer && + this->TargetMaterialLayer->GetPackage()->IsDirty()) { + UE_LOG( + LogCesium, + Error, + TEXT( + "Can't regenerate a material layer that has unsaved changes. Please save your changes and try again.")); + return; + } + + const FString MaterialName = + "ML_" + pTileset->GetFName().ToString() + "_VoxelMetadata"; + const FString PackageBaseName = "/Game/"; + const FString PackageName = PackageBaseName + MaterialName; + + bool Overwriting = false; + if (this->TargetMaterialLayer) { + // Overwriting an existing material layer. + Overwriting = true; + GEditor->GetEditorSubsystem() + ->CloseAllEditorsForAsset(this->TargetMaterialLayer); + } else { + this->TargetMaterialLayer = CreateMaterialLayer(PackageName, MaterialName); + } + + this->TargetMaterialLayer->PreEditChange(NULL); + + MaterialGenerationState MaterialState; + + ClearAutoGeneratedNodes(this->TargetMaterialLayer, MaterialState); + GenerateMaterialNodes(this, MaterialState, ResourceLibrary); + MoveNodesToMaterialLayer(MaterialState, this->TargetMaterialLayer); + + //RemapUserConnections( + // this->TargetMaterialLayer, + // MaterialState.ConnectionInputRemap, + // MaterialState.ConnectionOutputRemap); + + this->TargetMaterialLayer->PreviewBlendMode = + TEnumAsByte(EBlendMode::BLEND_Translucent); + + // Let the material update itself if necessary + this->TargetMaterialLayer->PostEditChange(); + + // Make sure that any static meshes, etc using this material will stop + // using the FMaterialResource of the original material, and will use the + // new FMaterialResource created when we make a new UMaterial in place + FGlobalComponentReregisterContext RecreateComponents; + + // If this is a new material, open the content browser to the auto-generated + // material. + if (!Overwriting) { + FContentBrowserModule* pContentBrowserModule = + FModuleManager::Get().GetModulePtr( + "ContentBrowser"); + if (pContentBrowserModule) { + TArray AssetsToHighlight; + AssetsToHighlight.Add(this->TargetMaterialLayer); + pContentBrowserModule->Get().SyncBrowserToAssets(AssetsToHighlight); + } + } + + // Open the updated material in editor. + if (GEditor) { + UAssetEditorSubsystem* pAssetEditor = + GEditor->GetEditorSubsystem(); + if (pAssetEditor) { + GEngine->EndTransaction(); + pAssetEditor->OpenEditorForAsset(this->TargetMaterialLayer); + IMaterialEditor* pMaterialEditor = static_cast( + pAssetEditor->FindEditorForAsset(this->TargetMaterialLayer, true)); + if (pMaterialEditor) { + pMaterialEditor->UpdateMaterialAfterGraphChange(); + } + } + } +} + +#endif // WITH_EDITOR diff --git a/Source/CesiumRuntime/Private/EncodedFeaturesMetadata.cpp b/Source/CesiumRuntime/Private/EncodedFeaturesMetadata.cpp index 67189f2d8..acf392e2f 100644 --- a/Source/CesiumRuntime/Private/EncodedFeaturesMetadata.cpp +++ b/Source/CesiumRuntime/Private/EncodedFeaturesMetadata.cpp @@ -971,6 +971,33 @@ bool isSupportedPropertyTextureProperty( return byteSize > 0 && byteSize <= 4; } +bool isSupportedPropertyAttributeProperty( + const FCesiumMetadataPropertyDetails& PropertyDetails) { + if (PropertyDetails.bIsArray) { + // Only types corresponding to glTF accessors are allowed. + return false; + } + + if (PropertyDetails.Type == ECesiumMetadataType::Mat2 || + PropertyDetails.Type == ECesiumMetadataType::Mat3 || + PropertyDetails.Type == ECesiumMetadataType::Mat4) { + // Matrix attributes are not (yet) supported. + return false; + } + + switch (PropertyDetails.ComponentType) { + case ECesiumMetadataComponentType::Uint8: + case ECesiumMetadataComponentType::Int8: + case ECesiumMetadataComponentType::Uint16: + case ECesiumMetadataComponentType::Int16: + case ECesiumMetadataComponentType::Uint32: + case ECesiumMetadataComponentType::Float32: + return true; + default: + return false; + } +} + void SetPropertyParameterValue( UMaterialInstanceDynamic* pMaterial, EMaterialParameterAssociation association, diff --git a/Source/CesiumRuntime/Private/EncodedFeaturesMetadata.h b/Source/CesiumRuntime/Private/EncodedFeaturesMetadata.h index ab6ac9819..022ba4363 100644 --- a/Source/CesiumRuntime/Private/EncodedFeaturesMetadata.h +++ b/Source/CesiumRuntime/Private/EncodedFeaturesMetadata.h @@ -524,6 +524,9 @@ FString createHlslSafeName(const FString& rawName); bool isSupportedPropertyTextureProperty( const FCesiumMetadataPropertyDetails& PropertyDetails); +bool isSupportedPropertyAttributeProperty( + const FCesiumMetadataPropertyDetails& PropertyDetails); + void SetPropertyParameterValue( UMaterialInstanceDynamic* pMaterial, EMaterialParameterAssociation association, diff --git a/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h b/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h index 9983a3d40..91640b730 100644 --- a/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h +++ b/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h @@ -225,6 +225,61 @@ struct CESIUMRUNTIME_API FCesiumPropertyTextureDescription { TArray Properties; }; +/** + * @brief Description of a property attribute property that should be encoded + * for access on the GPU. + * + * This is similar to FCesiumPropertyTablePropertyDescription, but is limited to + * the types that are supported for property attribute properties. + */ +USTRUCT() +struct CESIUMRUNTIME_API FCesiumPropertyAttributePropertyDescription { + GENERATED_USTRUCT_BODY() + + /** + * The name of this property. This will be how it is referenced in the + * material. + */ + UPROPERTY(EditAnywhere, Category = "Cesium") + FString Name; + + /** + * Describes the underlying type of this property and other relevant + * information from its EXT_structural_metadata definition. Not all types of + * properties can be encoded to the GPU, or coerced to GPU-compatible types. + */ + UPROPERTY(EditAnywhere, Category = "Cesium") + FCesiumMetadataPropertyDetails PropertyDetails; + + /** + * Describes how the property will be encoded as data on the GPU, if possible. + * TODO: Expose once coercive encoding is supported. + */ + // UPROPERTY(EditAnywhere, Category = "Cesium") + FCesiumMetadataEncodingDetails EncodingDetails; +}; + +/** + * @brief Description of a property attribute with properties that should be + * made accessible to Unreal materials. + */ +USTRUCT() +struct CESIUMRUNTIME_API FCesiumPropertyAttributeDescription { + GENERATED_USTRUCT_BODY() + + /** + * @brief The name of this property attribute. + */ + UPROPERTY(EditAnywhere, Category = "Cesium") + FString Name; + + /** + * @brief Descriptions of the properties to upload to the GPU. + */ + UPROPERTY(EditAnywhere, Category = "Cesium", Meta = (TitleProperty = "Name")) + TArray Properties; +}; + /** * @brief Names of the metadata entities referenced by the * EXT_structural_metadata on a glTF's primitives. diff --git a/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h new file mode 100644 index 000000000..50550c2a8 --- /dev/null +++ b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h @@ -0,0 +1,162 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumFeaturesMetadataDescription.h" +#include "CesiumMetadataComponent.h" +#include "Templates/UniquePtr.h" + +#if WITH_EDITOR +#include "Materials/MaterialFunctionMaterialLayer.h" +#endif + +#include "CesiumVoxelMetadataComponent.generated.h" + +class UVolumeTexture; + +/** + * @brief Description of the metadata properties available in the class used by + * the 3DTILES_content_voxels extension. Exposes what properties are available + * to use in a custom shader in Unreal materials. + */ +USTRUCT() struct CESIUMRUNTIME_API FCesiumVoxelClassDescription { + GENERATED_USTRUCT_BODY() + + /** + * @brief The ID of the class in the tileset's metadata schema. + */ + UPROPERTY( + EditAnywhere, + Category = "Metadata", + Meta = (TitleProperty = "Name")) + FString ID; + + /** + * @brief Descriptions of properties to pass to the Unreal material. + */ + UPROPERTY( + EditAnywhere, + Category = "Metadata", + Meta = (TitleProperty = "Name")) + TArray Properties; +}; + +/** + * @brief A component that can be added to Cesium3DTileset actors to + * view and style metadata embedded in voxels. The properties can be + * automatically populated by clicking the "Auto Fill" button. Once a selection + * of desired metadata is made, the boiler-plate material code to access the + * selected properties and apply custom shaders can be auto-generated using the + * "Generate Material" button. + */ +UCLASS(ClassGroup = (Cesium), Meta = (BlueprintSpawnableComponent)) +class CESIUMRUNTIME_API UCesiumVoxelMetadataComponent : public UActorComponent { + GENERATED_BODY() + +public: + UCesiumVoxelMetadataComponent(); + +#if WITH_EDITOR + /** + * Populate the description of metadata and feature IDs using the current view + * of the tileset. This determines what to encode to the GPU based on the + * existing metadata. + * + * Warning: Using Auto Fill may populate the description with a large amount + * of metadata. Make sure to delete the properties that aren't relevant. + */ + UFUNCTION(CallInEditor, Category = "Cesium") + void AutoFill(); + + /** + * This button can be used to create a boiler-plate material layer that + * exposes the requested metadata properties in the current description. The + * nodes to access the metadata will be added to TargetMaterialLayer if it + * exists. Otherwise a new material layer will be created in the /Content/ + * folder and TargetMaterialLayer will be set to the new material layer. + */ + UFUNCTION(CallInEditor, Category = "Cesium") + void GenerateMaterial(); +#endif + +#if WITH_EDITORONLY_DATA + /** + * This is the target UMaterialFunctionMaterialLayer that the + * boiler-plate material generation will use. When pressing + * "Generate Material", nodes will be added to this material to enable access + * to the requested metadata. If this is left blank, a new material layer + * will be created in the /Game/ folder. + */ + UPROPERTY(EditAnywhere, Category = "Cesium") + UMaterialFunctionMaterialLayer* TargetMaterialLayer = nullptr; +#endif + +#if WITH_EDITOR + /** + * A preview of the generated custom shader. + */ + UPROPERTY( + VisibleAnywhere, + Transient, + NonTransactional, + Category = "Cesium", + Meta = + (NoResetToDefault, + DisplayAfter = "TargetMaterialLayer", + MultiLine = true)) + FString CustomShaderPreview; + + /** + * The custom shader code to apply to each voxel that is raymarched. + */ + UPROPERTY( + EditAnywhere, + Category = "Cesium", + Meta = + (TitleProperty = "Custom Shader", + DisplayAfter = "CustomShaderPreview", + MultiLine = true)) + FString CustomShader = TEXT("return 1;"); + + /** + * Any additional functions to include for use in the custom shader. + */ + UPROPERTY( + EditAnywhere, + Category = "Cesium", + Meta = + (TitleProperty = "Additional Functions", + DisplayAfter = "CustomShader", + MultiLine = true)) + FString AdditionalFunctions; +#endif + + /** + * A description of the class used by the 3DTILES_content_voxel extension in + * the tileset. + */ + UPROPERTY( + EditAnywhere, + Category = "Cesium", + Meta = + (TitleProperty = "Voxel Class", DisplayAfter = "AdditionalFunctions")) + FCesiumVoxelClassDescription Description; + +protected: +#if WITH_EDITOR + virtual void PostLoad() override; + + virtual void + PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + virtual void PostEditChangeChainProperty( + FPropertyChangedChainEvent& PropertyChangedChainEvent) override; +#endif + +private: +#if WITH_EDITOR + UVolumeTexture* DefaultVolumeTexture; + static const FString ShaderPreviewTemplate; + + void UpdateShaderPreview(); +#endif +}; From 828cd4372ba7b8dbca89bac406b775dd329b54b4 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 6 Mar 2025 15:17:00 -0500 Subject: [PATCH 02/34] Add barebones glTF voxel component --- .../Private/CesiumGltfVoxelComponent.cpp | 16 +++++ .../Private/CesiumGltfVoxelComponent.h | 62 +++++++++++++++++++ .../CesiumRuntime/Private/CreateGltfOptions.h | 38 ++++++++++++ Source/CesiumRuntime/Private/LoadGltfResult.h | 17 +++++ Source/CesiumRuntime/Private/VoxelGridShape.h | 16 +++++ 5 files changed, 149 insertions(+) create mode 100644 Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp create mode 100644 Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h create mode 100644 Source/CesiumRuntime/Private/VoxelGridShape.h diff --git a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp new file mode 100644 index 000000000..53feaa35a --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp @@ -0,0 +1,16 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "CesiumGltfVoxelComponent.h" + +// Sets default values for this component's properties +UCesiumGltfVoxelComponent::UCesiumGltfVoxelComponent() { + PrimaryComponentTick.bCanEverTick = false; +} + +UCesiumGltfVoxelComponent::~UCesiumGltfVoxelComponent() {} + +void UCesiumGltfVoxelComponent::BeginDestroy() { + this->attributeBuffers.Empty(); + + Super::BeginDestroy(); +} diff --git a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h new file mode 100644 index 000000000..cca11bba8 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h @@ -0,0 +1,62 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CoreMinimal.h" +#include +#include + +#include "CesiumGltfVoxelComponent.generated.h" + +namespace CesiumGltf { +struct Model; +struct MeshPrimitive; +struct PropertyAttribute; +struct Accessor; +struct BufferView; +struct Buffer; +} // namespace CesiumGltf + + +class ACesium3DTileset; + +/** + * Pointers to a buffer that has already been validated, such that: + * - The accessor count is equal to the number of total voxels in the grid. + * - The buffer view on the buffer is valid. + * + * This should be replaced when PropertyAttributeProperty is supported, since it + * is functionally the same (and the latter would be more robust). + */ +struct ValidatedVoxelBuffer { + const CesiumGltf::Buffer* pBuffer; + const CesiumGltf::BufferView* pBufferView; +}; + +/** + * A barebones component representing a glTF voxel primitive. + * + * The voxel rendering for an entire tileset is done singlehandedly by + * UCesiumVoxelRendererComponent. Therefore, this component does not hold any + * mesh data itself. Instead, it stores pointers to the glTF primitive for easy + * retrieval of the voxel attributes. + */ +UCLASS() +class UCesiumGltfVoxelComponent : public USceneComponent { + GENERATED_BODY() + +public: + // Sets default values for this component's properties + UCesiumGltfVoxelComponent(); + virtual ~UCesiumGltfVoxelComponent(); + + void BeginDestroy(); + + ACesium3DTileset* pTilesetActor = nullptr; + const CesiumGltf::Model* pModel = nullptr; + const CesiumGltf::MeshPrimitive* pMeshPrimitive = nullptr; + + CesiumGeometry::OctreeTileID tileId; + + TMap attributeBuffers; +}; diff --git a/Source/CesiumRuntime/Private/CreateGltfOptions.h b/Source/CesiumRuntime/Private/CreateGltfOptions.h index ce8c7a5ee..1d8d0368f 100644 --- a/Source/CesiumRuntime/Private/CreateGltfOptions.h +++ b/Source/CesiumRuntime/Private/CreateGltfOptions.h @@ -16,6 +16,8 @@ * Various settings and options for loading a glTF model from a 3D Tileset. */ namespace CreateGltfOptions { +struct CreateVoxelOptions; + struct CreateModelOptions { /** * A pointer to the glTF model. @@ -52,6 +54,11 @@ struct CreateModelOptions { */ bool ignoreKhrMaterialsUnlit = false; + /** + * Options for loading voxel primitives in the tileset, if present. + */ + const CreateVoxelOptions* pVoxelOptions = nullptr; + Cesium3DTilesSelection::TileLoadResult tileLoadResult; public: @@ -67,6 +74,7 @@ struct CreateModelOptions { alwaysIncludeTangents(other.alwaysIncludeTangents), createPhysicsMeshes(other.createPhysicsMeshes), ignoreKhrMaterialsUnlit(other.ignoreKhrMaterialsUnlit), + pVoxelOptions(other.pVoxelOptions), tileLoadResult(std::move(other.tileLoadResult)) { pModel = std::get_if(&this->tileLoadResult.contentKind); } @@ -94,4 +102,34 @@ struct CreatePrimitiveOptions { const LoadGltfResult::LoadedMeshResult* pHalfConstructedMeshResult = nullptr; int32_t primitiveIndex = -1; }; + +/** + * Various settings and options for loading glTF voxels from a 3D Tileset. + * + * Currently these are used to validate voxels before construction, not so much + * for configuring their creation. + */ +struct CreateVoxelOptions { + /** + * The 3DTILES_content_voxels extension found on the tileset's root content. + */ + const Cesium3DTiles::ExtensionContent3dTilesContentVoxels* pTilesetExtension; + + /** + * A pointer to the class used by the tileset to define the voxel metadata. + */ + const Cesium3DTiles::Class* pVoxelClass; + + /** + * The shape of the voxel grid. + */ + EVoxelGridShape gridShape; + + /** + * The total number of voxels in the voxel grid, including padding. Used to + * validate glTF voxel primitives for their amounts of attribute data. + */ + size_t voxelCount; +}; + } // namespace CreateGltfOptions diff --git a/Source/CesiumRuntime/Private/LoadGltfResult.h b/Source/CesiumRuntime/Private/LoadGltfResult.h index 38cc9a0df..88aafc834 100644 --- a/Source/CesiumRuntime/Private/LoadGltfResult.h +++ b/Source/CesiumRuntime/Private/LoadGltfResult.h @@ -29,6 +29,13 @@ #include namespace LoadGltfResult { +/** + * Represents the result of loading a glTF voxel primitive on a load thread. + */ +struct LoadVoxelResult { + TMap attributeBuffers; +}; + /** * Represents the result of loading a glTF primitive on a load thread. * Temporarily holds render data that will be used in the Unreal material, as @@ -153,6 +160,8 @@ struct LoadedPrimitiveResult { CesiumGltf::IndexAccessorType IndexAccessor; #pragma endregion + + std::optional voxelResult = std::nullopt; }; /** @@ -207,5 +216,13 @@ struct LoadedModelResult { /** For backwards compatibility with CesiumEncodedMetadataComponent. */ std::optional EncodedMetadata_DEPRECATED{}; + + /** + * Points to the actual EXT_structural_metadata extension. Used for voxels + * because property attributes are not yet supported. + * + * TODO: Expand FCesiumModelMetadata so this is not necessary. + */ + const CesiumGltf::ExtensionModelExtStructuralMetadata* pMetadata = nullptr; }; } // namespace LoadGltfResult diff --git a/Source/CesiumRuntime/Private/VoxelGridShape.h b/Source/CesiumRuntime/Private/VoxelGridShape.h new file mode 100644 index 000000000..880914c83 --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelGridShape.h @@ -0,0 +1,16 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CoreMinimal.h" + +/** + * @brief Enumeration of the supported voxel grid shapes in the + * `3DTILES_content_voxels` extension. + */ +enum class EVoxelGridShape : uint8 { + Invalid = 0, + Box = 1, + Cylinder = 2, + Ellipsoid = 3 +}; From f9366e9ec062ef57460f70284c912ddb8cb66496 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 6 Mar 2025 15:47:21 -0500 Subject: [PATCH 03/34] Add rendering architecture for voxels --- .../CesiumRuntime/Private/Cesium3DTileset.cpp | 160 ++++-- .../Private/CesiumVoxelMetadataComponent.cpp | 18 +- .../Private/CesiumVoxelRendererComponent.cpp | 544 ++++++++++++++++++ .../Private/CesiumVoxelRendererComponent.h | 110 ++++ .../CesiumRuntime/Private/CreateGltfOptions.h | 2 + Source/CesiumRuntime/Private/LoadGltfResult.h | 2 + .../UnrealPrepareRendererResources.cpp | 6 + .../Private/VoxelDataTextures.cpp | 356 ++++++++++++ .../CesiumRuntime/Private/VoxelDataTextures.h | 146 +++++ Source/CesiumRuntime/Private/VoxelOctree.cpp | 487 ++++++++++++++++ Source/CesiumRuntime/Private/VoxelOctree.h | 256 +++++++++ .../CesiumRuntime/Private/VoxelResources.cpp | 228 ++++++++ Source/CesiumRuntime/Private/VoxelResources.h | 118 ++++ Source/CesiumRuntime/Public/Cesium3DTileset.h | 22 +- .../Public/CesiumVoxelMetadataComponent.h | 3 - 15 files changed, 2406 insertions(+), 52 deletions(-) create mode 100644 Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp create mode 100644 Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h create mode 100644 Source/CesiumRuntime/Private/VoxelDataTextures.cpp create mode 100644 Source/CesiumRuntime/Private/VoxelDataTextures.h create mode 100644 Source/CesiumRuntime/Private/VoxelOctree.cpp create mode 100644 Source/CesiumRuntime/Private/VoxelOctree.h create mode 100644 Source/CesiumRuntime/Private/VoxelResources.cpp create mode 100644 Source/CesiumRuntime/Private/VoxelResources.h diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 3978628e9..72f994f01 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -4,32 +4,25 @@ #include "Async/Async.h" #include "Camera/CameraTypes.h" #include "Camera/PlayerCameraManager.h" -#include "Cesium3DTilesSelection/EllipsoidTilesetLoader.h" -#include "Cesium3DTilesSelection/Tile.h" -#include "Cesium3DTilesSelection/TilesetLoadFailureDetails.h" -#include "Cesium3DTilesSelection/TilesetOptions.h" -#include "Cesium3DTilesSelection/TilesetSharedAssetSystem.h" #include "Cesium3DTilesetLoadFailureDetails.h" #include "Cesium3DTilesetRoot.h" #include "CesiumActors.h" -#include "CesiumAsync/SharedAssetDepot.h" #include "CesiumBoundingVolumeComponent.h" #include "CesiumCamera.h" #include "CesiumCameraManager.h" #include "CesiumCommon.h" #include "CesiumCustomVersion.h" -#include "CesiumGeospatial/GlobeTransforms.h" -#include "CesiumGltf/ImageAsset.h" -#include "CesiumGltf/Ktx2TranscodeTargets.h" +#include "CesiumFeaturesMetadataComponent.h" #include "CesiumGltfComponent.h" #include "CesiumGltfPointsSceneProxyUpdater.h" #include "CesiumGltfPrimitiveComponent.h" -#include "CesiumIonClient/Connection.h" +#include "CesiumLifetime.h" #include "CesiumRasterOverlay.h" #include "CesiumRuntime.h" #include "CesiumRuntimeSettings.h" #include "CesiumTileExcluder.h" #include "CesiumViewExtension.h" +#include "CesiumVoxelRendererComponent.h" #include "Components/SceneCaptureComponent2D.h" #include "Engine/Engine.h" #include "Engine/LocalPlayer.h" @@ -48,6 +41,19 @@ #include "StereoRendering.h" #include "UnrealPrepareRendererResources.h" #include "VecMath.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include #include #include @@ -363,6 +369,8 @@ void ACesium3DTileset::PostInitProperties() { } } +#pragma region Getters / Setters + void ACesium3DTileset::SetUseLodTransitions(bool InUseLodTransitions) { if (InUseLodTransitions != this->UseLodTransitions) { this->UseLodTransitions = InUseLodTransitions; @@ -574,6 +582,8 @@ void ACesium3DTileset::SetTranslucencySortPriority( } } +#pragma endregion + void ACesium3DTileset::PlayMovieSequencer() { this->_beforeMoviePreloadAncestors = this->PreloadAncestors; this->_beforeMoviePreloadSiblings = this->PreloadSiblings; @@ -737,6 +747,10 @@ void ACesium3DTileset::UpdateTransformFromCesium() { this->BoundingVolumePoolComponent->UpdateTransformFromCesium( CesiumToUnreal); } + + if (this->_pVoxelRendererComponent) { + this->_pVoxelRendererComponent->UpdateTransformFromCesium(CesiumToUnreal); + } } void ACesium3DTileset::HandleOnGeoreferenceEllipsoidChanged( @@ -931,7 +945,6 @@ void ACesium3DTileset::LoadTileset() { // Check if this component exists for backwards compatibility. PRAGMA_DISABLE_DEPRECATION_WARNINGS - const UDEPRECATED_CesiumEncodedMetadataComponent* pEncodedMetadataComponent = this->FindComponentByClass(); @@ -951,9 +964,14 @@ void ACesium3DTileset::LoadTileset() { pEncodedMetadataComponent->FeatureTables, pEncodedMetadataComponent->FeatureTextures}; } - PRAGMA_ENABLE_DEPRECATION_WARNINGS + const UCesiumVoxelMetadataComponent* pVoxelMetadataComponent = + this->FindComponentByClass(); + if (pVoxelMetadataComponent) { + this->_voxelClassDescription = pVoxelMetadataComponent->Description; + } + this->_cesiumViewExtension = cesiumViewExtension; if (GetDefault() @@ -1150,6 +1168,14 @@ void ACesium3DTileset::LoadTileset() { TCHAR_TO_UTF8(*dbFile)); #endif + this->_pTileset->getRootTileAvailableEvent().thenImmediately([thiz = this]() { + const Cesium3DTiles::ExtensionContent3dTilesContentVoxels* pVoxelExtension = + thiz->_pTileset ? thiz->_pTileset->getVoxelContentExtension() : nullptr; + if (pVoxelExtension) { + thiz->initializeVoxelRenderer(*pVoxelExtension); + } + }); + for (UCesiumRasterOverlay* pOverlay : rasterOverlays) { if (pOverlay->IsActive()) { pOverlay->AddToTileset(); @@ -1243,6 +1269,11 @@ void ACesium3DTileset::DestroyTileset() { } } + if (this->_pVoxelRendererComponent) { + CesiumLifetime::destroyComponentRecursively(this->_pVoxelRendererComponent); + this->_pVoxelRendererComponent = nullptr; + } + if (!this->_pTileset) { return; } @@ -1308,6 +1339,8 @@ std::vector ACesium3DTileset::GetCameras() const { return cameras; } +#pragma region Camera Collections + std::vector ACesium3DTileset::GetPlayerCameras() const { UWorld* pWorld = this->GetWorld(); if (!pWorld) { @@ -1642,6 +1675,8 @@ std::vector ACesium3DTileset::GetEditorCameras() const { } #endif +#pragma endregion + bool ACesium3DTileset::ShouldTickIfViewportsOnly() const { return this->UpdateInEditor; } @@ -1734,12 +1769,11 @@ void removeCollisionForTiles( } /** - * @brief Applies the actor collision settings for a newly created glTF - * component + * @brief Applies the specified collision profile to the glTF component + * and its children. * - * TODO Add details here what that means - * @param BodyInstance ... - * @param Gltf ... + * @param BodyInstance The collision profile. + * @param Gltf The target glTF component. */ void applyActorCollisionSettings( const FBodyInstance& BodyInstance, @@ -2048,30 +2082,36 @@ void ACesium3DTileset::Tick(float DeltaTime) { } updateLastViewUpdateResultState(*pResult); - removeCollisionForTiles(pResult->tilesFadingOut); - removeVisibleTilesFromList( - _tilesToHideNextFrame, + this->_tilesToHideNextFrame, pResult->tilesToRenderThisFrame); - hideTiles(_tilesToHideNextFrame); - - _tilesToHideNextFrame.clear(); - for (Cesium3DTilesSelection::Tile* pTile : pResult->tilesFadingOut) { - Cesium3DTilesSelection::TileRenderContent* pRenderContent = - pTile->getContent().getRenderContent(); - if (!this->UseLodTransitions || - (pRenderContent && - pRenderContent->getLodTransitionFadePercentage() >= 1.0f)) { - _tilesToHideNextFrame.push_back(pTile); + + if (this->_pVoxelRendererComponent) { + this->_pVoxelRendererComponent->UpdateTiles( + pResult->tilesToRenderThisFrame, + pResult->tileScreenSpaceErrorThisFrame); + } else { + removeCollisionForTiles(pResult->tilesFadingOut); + hideTiles(_tilesToHideNextFrame); + + _tilesToHideNextFrame.clear(); + for (Cesium3DTilesSelection::Tile* pTile : pResult->tilesFadingOut) { + Cesium3DTilesSelection::TileRenderContent* pRenderContent = + pTile->getContent().getRenderContent(); + if (!this->UseLodTransitions || + (pRenderContent && + pRenderContent->getLodTransitionFadePercentage() >= 1.0f)) { + _tilesToHideNextFrame.push_back(pTile); + } } - } - showTilesToRender(pResult->tilesToRenderThisFrame); + showTilesToRender(pResult->tilesToRenderThisFrame); - if (this->UseLodTransitions) { - TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::UpdateTileFades) - updateTileFades(pResult->tilesToRenderThisFrame, true); - updateTileFades(pResult->tilesFadingOut, false); + if (this->UseLodTransitions) { + TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::UpdateTileFades) + updateTileFades(pResult->tilesToRenderThisFrame, true); + updateTileFades(pResult->tilesFadingOut, false); + } } this->UpdateLoadStatus(); @@ -2289,3 +2329,51 @@ void ACesium3DTileset::RuntimeSettingsChanged( } } #endif + +void ACesium3DTileset::initializeVoxelRenderer( + const Cesium3DTiles::ExtensionContent3dTilesContentVoxels& VoxelExtension) { + const FCesiumVoxelClassDescription* pVoxelClassDescription = + this->_voxelClassDescription ? &(*this->_voxelClassDescription) : nullptr; + + // Validate that voxel metadata is present. + const Cesium3DTilesSelection::TilesetMetadata* pMetadata = + this->_pTileset->getMetadata(); + if (!pMetadata || !pMetadata->schema) { + UE_LOG( + LogCesium, + Error, + TEXT( + "Tileset %s contains voxels, but is missing a metadata schema to describe its contents."), + *this->GetName()) + return; + } + + const Cesium3DTilesSelection::Tile* pRootTile = + this->_pTileset->getRootTile(); + if (!pRootTile) { + // Not sure how this could happen, but just in case... + CESIUM_ASSERT(false); + return; + } + + this->_pVoxelRendererComponent = UCesiumVoxelRendererComponent::Create( + this, + *pMetadata, + *pRootTile, + VoxelExtension, + pVoxelClassDescription); + + if (this->_pVoxelRendererComponent) { + // The AttachToComponent method is ridiculously complex, + // so print a warning if attaching fails for some reason + bool attached = this->_pVoxelRendererComponent->AttachToComponent( + this->RootComponent, + FAttachmentTransformRules::KeepRelativeTransform); + if (!attached) { + UE_LOG( + LogCesium, + Warning, + TEXT("Voxel renderer could not be attached to root")); + } + } +} diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index e093609ce..a4d46c6a0 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -49,12 +49,6 @@ #include -#if ENGINE_VERSION_5_3_OR_HIGHER -#define GET_INPUTS_MEMBER GetInputsView -#else -#define GET_INPUTS_MEMBER GetInputs -#endif - using namespace EncodedFeaturesMetadata; using namespace GenerateMaterialUtility; @@ -663,8 +657,8 @@ static void GenerateMaterialNodes( // duplicated, point the reference to that new expression. Otherwise, clear // the input. for (UMaterialExpression* NewExpression : MaterialState.AutoGeneratedNodes) { - const TArray& ExpressionInputs = - NewExpression->GetInputs(); + const TArrayView& ExpressionInputs = + NewExpression->GetInputsView(); for (int32 ExpressionInputIndex = 0; ExpressionInputIndex < ExpressionInputs.Num(); ++ExpressionInputIndex) { @@ -900,10 +894,10 @@ void UCesiumVoxelMetadataComponent::GenerateMaterial() { GenerateMaterialNodes(this, MaterialState, ResourceLibrary); MoveNodesToMaterialLayer(MaterialState, this->TargetMaterialLayer); - //RemapUserConnections( - // this->TargetMaterialLayer, - // MaterialState.ConnectionInputRemap, - // MaterialState.ConnectionOutputRemap); + // RemapUserConnections( + // this->TargetMaterialLayer, + // MaterialState.ConnectionInputRemap, + // MaterialState.ConnectionOutputRemap); this->TargetMaterialLayer->PreviewBlendMode = TEnumAsByte(EBlendMode::BLEND_Translucent); diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp new file mode 100644 index 000000000..b702e7e72 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -0,0 +1,544 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "CesiumVoxelRendererComponent.h" +#include "CalcBounds.h" +#include "Cesium3DTileset.h" +#include "CesiumLifetime.h" +#include "CesiumMaterialUserData.h" +#include "CesiumRuntime.h" +#include "CesiumVoxelMetadataComponent.h" +#include "EncodedFeaturesMetadata.h" +#include "Engine/StaticMesh.h" +#include "Engine/Texture.h" +#include "Materials/MaterialInstanceDynamic.h" +#include "PhysicsEngine/BodySetup.h" +#include "UObject/ConstructorHelpers.h" +#include "VecMath.h" + +#include +#include +#include +#include +#include +#include + +// Sets default values for this component's properties +UCesiumVoxelRendererComponent::UCesiumVoxelRendererComponent() + : USceneComponent() { + // Structure to hold one-time initialization + struct FConstructorStatics { + ConstructorHelpers::FObjectFinder DefaultMaterial; + ConstructorHelpers::FObjectFinder CubeMesh; + FConstructorStatics() + : DefaultMaterial(TEXT( + "/CesiumForUnreal/Materials/Instances/MI_CesiumVoxels.MI_CesiumVoxels")), + CubeMesh(TEXT("/Engine/BasicShapes/Cube.Cube")) {} + }; + static FConstructorStatics ConstructorStatics; + + this->DefaultMaterial = ConstructorStatics.DefaultMaterial.Object; + this->CubeMesh = ConstructorStatics.CubeMesh.Object; + this->CubeMesh->NeverStream = true; + + PrimaryComponentTick.bCanEverTick = false; +} + +UCesiumVoxelRendererComponent::~UCesiumVoxelRendererComponent() {} + +void UCesiumVoxelRendererComponent::BeginDestroy() { + if (this->MeshComponent) { + // Only handle the destruction of the material instance. The + // UStaticMeshComponent attached to this component will be destroyed by the + // call to destroyComponentRecursively in Cesium3DTileset.cpp. + UMaterialInstanceDynamic* pMaterial = + Cast(MeshComponent->GetMaterial(0)); + if (pMaterial) { + CesiumLifetime::destroy(pMaterial); + } + } + + // Reset the pointers. + this->MeshComponent = nullptr; + this->_pResources.Reset(); + + Super::BeginDestroy(); +} + +namespace { +EVoxelGridShape getVoxelGridShape( + const Cesium3DTilesSelection::BoundingVolume& boundingVolume) { + const CesiumGeometry::OrientedBoundingBox* pBox = + std::get_if(&boundingVolume); + if (pBox) { + return EVoxelGridShape::Box; + } + + return EVoxelGridShape::Invalid; +} + +void setVoxelBoxProperties( + UCesiumVoxelRendererComponent* pVoxelComponent, + UMaterialInstanceDynamic* pVoxelMaterial, + const CesiumGeometry::OrientedBoundingBox& box) { + 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 transform and scale of the box 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)); +} + +FCesiumMetadataValue +getMetadataValue(const std::optional& jsonValue) { + if (!jsonValue) + return FCesiumMetadataValue(); + + if (jsonValue->isArray()) { + CesiumUtility::JsonValue::Array array = jsonValue->getArray(); + if (array.size() == 0 || array.size() > 4) { + return FCesiumMetadataValue(); + } + + // Attempt to convert the array to a vec4 (or a value with less dimensions). + size_t endIndex = FMath::Min(array.size(), (size_t)4); + TArray values; + for (size_t i = 0; i < endIndex; i++) { + values.Add(UCesiumMetadataValueBlueprintLibrary::GetFloat( + getMetadataValue(array[i]), + 0.0f)); + } + + switch (values.Num()) { + case 1: + return FCesiumMetadataValue(values[0]); + case 2: + return FCesiumMetadataValue(glm::vec2(values[0], values[1])); + case 3: + return FCesiumMetadataValue(glm::vec3(values[0], values[1], values[2])); + case 4: + return FCesiumMetadataValue( + glm::vec4(values[0], values[1], values[2], values[3])); + default: + return FCesiumMetadataValue(); + } + } + + if (jsonValue->isInt64()) { + return FCesiumMetadataValue(jsonValue->getInt64OrDefault(0)); + } + + if (jsonValue->isUint64()) { + return FCesiumMetadataValue(jsonValue->getUint64OrDefault(0)); + } + + if (jsonValue->isDouble()) { + return FCesiumMetadataValue(jsonValue->getDoubleOrDefault(0.0)); + } + + return FCesiumMetadataValue(); +} + +// uint32 getMaximumTextureMemory( +// const FCesiumVoxelClassDescription* pDescription, +// const glm::uvec3& gridDimensions, +// uint64_t tileCount) { +// int32_t pixelSize = 0; +// +// if (pDescription) { +// for (const FCesiumPropertyAttributePropertyDescription& Property : +// pDescription->Properties) { +// EncodedFeaturesMetadata::EncodedPixelFormat pixelFormat = +// EncodedFeaturesMetadata::getPixelFormat( +// Property.EncodingDetails.Type, +// Property.EncodingDetails.ComponentType); +// pixelSize = FMath::Max( +// pixelSize, +// pixelFormat.bytesPerChannel * pixelFormat.channels); +// } +// } +// +// return (uint32)pixelSize * gridDimensions.x * gridDimensions.y * +// gridDimensions.z * tileCount; +// } + +} // namespace + +/*static*/ UMaterialInstanceDynamic* +UCesiumVoxelRendererComponent::CreateVoxelMaterial( + UCesiumVoxelRendererComponent* pVoxelComponent, + const FVector& dimensions, + const FVector& paddingBefore, + const FVector& paddingAfter, + ACesium3DTileset* pTilesetActor, + const Cesium3DTiles::Class* pVoxelClass, + const FCesiumVoxelClassDescription* pDescription, + const Cesium3DTilesSelection::BoundingVolume& boundingVolume) { + UMaterialInterface* pMaterial = pTilesetActor->GetMaterial(); + + UMaterialInstanceDynamic* pVoxelMaterial = UMaterialInstanceDynamic::Create( + pMaterial ? pMaterial : pVoxelComponent->DefaultMaterial, + nullptr, + FName("VoxelMaterial")); + pVoxelMaterial->SetFlags( + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + + EVoxelGridShape shape = pVoxelComponent->Options.gridShape; + + pVoxelMaterial->SetTextureParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Octree"), + EMaterialParameterAssociation::LayerParameter, + 0), + pVoxelComponent->_pResources->GetOctreeTexture()); + pVoxelMaterial->SetScalarParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Shape Constant"), + EMaterialParameterAssociation::LayerParameter, + 0), + uint8(shape)); + + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Grid Dimensions"), + EMaterialParameterAssociation::LayerParameter, + 0), + dimensions); + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Padding Before"), + EMaterialParameterAssociation::LayerParameter, + 0), + paddingBefore); + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Padding After"), + EMaterialParameterAssociation::LayerParameter, + 0), + paddingAfter); + + if (shape == EVoxelGridShape::Box) { + const CesiumGeometry::OrientedBoundingBox* pBox = + std::get_if(&boundingVolume); + assert(pBox != nullptr); + setVoxelBoxProperties(pVoxelComponent, pVoxelMaterial, *pBox); + } + + if (pDescription && pVoxelClass) { + for (const auto propertyIt : pVoxelClass->properties) { + FString UnrealName(propertyIt.first.c_str()); + + for (const FCesiumPropertyAttributePropertyDescription& Property : + pDescription->Properties) { + if (Property.Name != UnrealName) { + continue; + } + + FString PropertyName = + EncodedFeaturesMetadata::createHlslSafeName(Property.Name); + + pVoxelMaterial->SetTextureParameterValueByInfo( + FMaterialParameterInfo( + FName(PropertyName), + EMaterialParameterAssociation::LayerParameter, + 0), + pVoxelComponent->_pResources->GetDataTexture(Property.Name)); + + if (Property.PropertyDetails.bHasScale) { + EncodedFeaturesMetadata::SetPropertyParameterValue( + pVoxelMaterial, + EMaterialParameterAssociation::LayerParameter, + 0, + PropertyName + + EncodedFeaturesMetadata::MaterialPropertyScaleSuffix, + Property.EncodingDetails.Type, + getMetadataValue(propertyIt.second.scale), + 1); + } + + if (Property.PropertyDetails.bHasOffset) { + EncodedFeaturesMetadata::SetPropertyParameterValue( + pVoxelMaterial, + EMaterialParameterAssociation::LayerParameter, + 0, + PropertyName + + EncodedFeaturesMetadata::MaterialPropertyOffsetSuffix, + Property.EncodingDetails.Type, + getMetadataValue(propertyIt.second.offset), + 0); + } + + if (Property.PropertyDetails.bHasNoDataValue) { + EncodedFeaturesMetadata::SetPropertyParameterValue( + pVoxelMaterial, + EMaterialParameterAssociation::LayerParameter, + 0, + PropertyName + + EncodedFeaturesMetadata::MaterialPropertyNoDataSuffix, + Property.EncodingDetails.Type, + getMetadataValue(propertyIt.second.noData), + 0); + } + + if (Property.PropertyDetails.bHasDefaultValue) { + EncodedFeaturesMetadata::SetPropertyParameterValue( + pVoxelMaterial, + EMaterialParameterAssociation::LayerParameter, + 0, + PropertyName + + EncodedFeaturesMetadata::MaterialPropertyDefaultValueSuffix, + Property.EncodingDetails.Type, + getMetadataValue(propertyIt.second.defaultProperty), + 0); + } + } + } + + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Tile Count"), + EMaterialParameterAssociation::LayerParameter, + 0), + pVoxelComponent->_pResources->GetTileCount()); + } + + return pVoxelMaterial; +} + +/*static*/ UCesiumVoxelRendererComponent* UCesiumVoxelRendererComponent::Create( + ACesium3DTileset* pTilesetActor, + const Cesium3DTilesSelection::TilesetMetadata& tilesetMetadata, + const Cesium3DTilesSelection::Tile& rootTile, + const Cesium3DTiles::ExtensionContent3dTilesContentVoxels& voxelExtension, + const FCesiumVoxelClassDescription* pDescription) { + if (!pTilesetActor) { + return nullptr; + } + + const std::string& voxelClassId = voxelExtension.classProperty; + if (tilesetMetadata.schema->classes.find(voxelClassId) == + tilesetMetadata.schema->classes.end()) { + UE_LOG( + LogCesium, + Error, + TEXT( + "Tileset %s contains voxels, but cannot find the metadata class that describes its contents."), + *pTilesetActor->GetName()) + return nullptr; + } + + // Validate voxel grid dimensions. + const std::vector dimensions = voxelExtension.dimensions; + if (dimensions.size() < 3 || dimensions[0] <= 0 || dimensions[1] <= 0 || + dimensions[2] <= 0) { + UE_LOG( + LogCesium, + Error, + TEXT("Tileset %s contains voxels but has invalid dimensions."), + *pTilesetActor->GetName()) + return nullptr; + } + + // Validate voxel grid padding, if present. + glm::uvec3 paddingBefore(0); + glm::uvec3 paddingAfter(0); + + if (voxelExtension.padding) { + const std::vector before = voxelExtension.padding->before; + if (before.size() != 3 || before[0] < 0 || before[1] < 0 || before[2] < 0) { + UE_LOG( + LogCesium, + Error, + TEXT( + "Tileset %s has invalid value for padding.before in its voxel extension."), + *pTilesetActor->GetName()) + return nullptr; + } + + const std::vector after = voxelExtension.padding->after; + if (after.size() != 3 || after[0] < 0 || after[1] < 0 || after[2] < 0) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Tileset %s has invalid value for padding.after in its voxel extension."), + *pTilesetActor->GetName()) + return nullptr; + } + + paddingBefore = {before[0], before[1], before[2]}; + paddingAfter = {after[0], after[1], after[2]}; + } + + // Check that bounding volume is supported. + const Cesium3DTilesSelection::BoundingVolume& boundingVolume = + rootTile.getBoundingVolume(); + EVoxelGridShape shape = getVoxelGridShape(boundingVolume); + if (shape == EVoxelGridShape::Invalid) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Tileset %s has invalid value for padding.after in its voxel extension."), + *pTilesetActor->GetName()) + return nullptr; + } + + const Cesium3DTiles::Class* pVoxelClass = + &tilesetMetadata.schema->classes.at(voxelClassId); + assert(pVoxelClass != nullptr); + + UCesiumVoxelRendererComponent* pVoxelComponent = + NewObject(pTilesetActor); + pVoxelComponent->SetMobility(pTilesetActor->GetRootComponent()->Mobility); + pVoxelComponent->SetFlags( + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + pVoxelComponent->_pTileset = pTilesetActor; + + UStaticMeshComponent* pVoxelMesh = + NewObject(pVoxelComponent); + pVoxelMesh->SetStaticMesh(pVoxelComponent->CubeMesh); + pVoxelMesh->SetFlags( + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + pVoxelMesh->SetMobility(pVoxelComponent->Mobility); + pVoxelMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); + + FCustomDepthParameters customDepthParameters = + pTilesetActor->GetCustomDepthParameters(); + + pVoxelMesh->SetRenderCustomDepth(customDepthParameters.RenderCustomDepth); + pVoxelMesh->SetCustomDepthStencilWriteMask( + customDepthParameters.CustomDepthStencilWriteMask); + pVoxelMesh->SetCustomDepthStencilValue( + customDepthParameters.CustomDepthStencilValue); + pVoxelMesh->bCastDynamicShadow = false; + + pVoxelMesh->SetupAttachment(pVoxelComponent); + pVoxelMesh->RegisterComponent(); + + pVoxelComponent->MeshComponent = pVoxelMesh; + + // The expected size of the incoming glTF attributes depends on padding and + // voxel grid shape. + glm::uvec3 dataDimensions = + glm::uvec3(dimensions[0], dimensions[1], dimensions[2]) + paddingBefore + + paddingAfter; + if (shape == EVoxelGridShape::Box || shape == EVoxelGridShape::Cylinder) { + // Account for y-up in glTF -> z-up in 3D Tiles. + dataDimensions = + glm::uvec3(dataDimensions.x, dataDimensions.z, dataDimensions.y); + } + + uint32 requestedTextureMemory = + FVoxelResources::DefaultDataTextureMemoryBytes; + + // uint64_t knownTileCount = 0; + // if (tilesetMetadata.metadata) { + // const Cesium3DTiles::MetadataEntity& metadata = + // *tilesetMetadata.metadata; + // // TODO: This should find the property by "TILESET_TILE_COUNT" + // if (metadata.properties.find("tileCount") != metadata.properties.end()) { + // const CesiumUtility::JsonValue& value = + // metadata.properties.at("tileCount"); + // if (value.isInt64()) { + // knownTileCount = value.getInt64OrDefault(0); + // } else if (value.isUint64()) { + // knownTileCount = value.getUint64OrDefault(0); + // } + // } + // } + + // if (knownTileCount > 0) { + // uint32 maximumTextureMemory = + // getMaximumTextureMemory(pDescription, dataDimensions, knownTileCount); + // requestedTextureMemory = FMath::Min( + // maximumTextureMemory, + // FVoxelResources::MaximumDataTextureMemoryBytes); + //} + + pVoxelComponent->_pResources = MakeUnique( + pDescription, + shape, + dataDimensions, + pVoxelMesh->GetScene()->GetFeatureLevel(), + requestedTextureMemory); + + CreateGltfOptions::CreateVoxelOptions& options = pVoxelComponent->Options; + options.pTilesetExtension = &voxelExtension; + options.pVoxelClass = pVoxelClass; + options.gridShape = shape; + options.voxelCount = dataDimensions.x * dataDimensions.y * dataDimensions.z; + + UMaterialInstanceDynamic* pMaterial = + UCesiumVoxelRendererComponent::CreateVoxelMaterial( + pVoxelComponent, + FVector(dimensions[0], dimensions[1], dimensions[2]), + FVector(paddingBefore.x, paddingBefore.y, paddingBefore.z), + FVector(paddingAfter.x, paddingAfter.y, paddingAfter.z), + pTilesetActor, + pVoxelClass, + pDescription, + boundingVolume); + pVoxelMesh->SetMaterial(0, pMaterial); + + const glm::dmat4& cesiumToUnrealTransform = + pTilesetActor->GetCesiumTilesetToUnrealRelativeWorldTransform(); + pVoxelComponent->UpdateTransformFromCesium(cesiumToUnrealTransform); + + return pVoxelComponent; +} + +void UCesiumVoxelRendererComponent::UpdateTiles( + const std::vector& VisibleTiles, + const std::vector& VisibleTileScreenSpaceErrors) { + this->_pResources->Update(VisibleTiles, VisibleTileScreenSpaceErrors); +} + +void UCesiumVoxelRendererComponent::UpdateTransformFromCesium( + const glm::dmat4& CesiumToUnrealTransform) { + FTransform transform = FTransform(VecMath::createMatrix( + CesiumToUnrealTransform * this->HighPrecisionTransform)); + + if (this->MeshComponent->Mobility == EComponentMobility::Movable) { + // For movable objects, move the component in the normal way, but don't + // generate collisions along the way. Teleporting physics is imperfect, + // but it's the best available option. + this->MeshComponent->SetRelativeTransform( + transform, + false, + nullptr, + ETeleportType::TeleportPhysics); + } else { + // Unreal will yell at us for calling SetRelativeTransform on a static + // object, but we still need to adjust (accurately!) for origin rebasing + // and georeference changes. It's "ok" to move a static object in this way + // because, we assume, the globe and globe-oriented lights, etc. are + // moving too, so in a relative sense the object isn't actually moving. + // This isn't a perfect assumption, of course. + this->MeshComponent->SetRelativeTransform_Direct(transform); + this->MeshComponent->UpdateComponentToWorld(); + this->MeshComponent->MarkRenderTransformDirty(); + } +} diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h new file mode 100644 index 000000000..ef1848a19 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h @@ -0,0 +1,110 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "VoxelResources.h" +#include "Components/SceneComponent.h" +#include "Components/StaticMeshComponent.h" +#include "CoreMinimal.h" +#include "CreateGltfOptions.h" +#include "CustomDepthParameters.h" +#include "Materials/MaterialInstanceDynamic.h" +#include "Templates/UniquePtr.h" +#include "VoxelGridShape.h" + +#include +#include + +#include "CesiumVoxelRendererComponent.generated.h" + +namespace Cesium3DTilesSelection { +class Tile; +class TilesetMetadata; +} // namespace Cesium3DTilesSelection + +class ACesium3DTileset; +struct FCesiumVoxelClassDescription; + +UCLASS() +/** + * A component that enables raycasted voxel rendering. This is only attached to + * a Cesium3DTileset when it contains voxel data. + * + * Unlike typical glTF meshes, voxels are rendered by raycasting in an Unreal + * material attached to a placeholder cube mesh. + */ +class UCesiumVoxelRendererComponent : public USceneComponent { + GENERATED_BODY() + +public: + static UCesiumVoxelRendererComponent* Create( + ACesium3DTileset* pTilesetActor, + const Cesium3DTilesSelection::TilesetMetadata& tilesetMetadata, + const Cesium3DTilesSelection::Tile& rootTile, + const Cesium3DTiles::ExtensionContent3dTilesContentVoxels& voxelExtension, + const FCesiumVoxelClassDescription* pDescription); + + // Sets default values for this component's properties + UCesiumVoxelRendererComponent(); + virtual ~UCesiumVoxelRendererComponent(); + + void BeginDestroy() override; + UPROPERTY(EditAnywhere, Category = "Cesium") + UMaterialInterface* DefaultMaterial = nullptr; + + UPROPERTY(EditAnywhere, Category = "Cesium") + UStaticMesh* CubeMesh = nullptr; + + /** + * The mesh used to render the voxels. + */ + UStaticMeshComponent* MeshComponent = nullptr; + + /** + * The options for creating voxel primitives based on the tileset's + * 3DTILES_content_voxels extension. This is referenced during the glTF load + * process. + */ + CreateGltfOptions::CreateVoxelOptions Options; + + /** + * The double-precision transformation matrix for the root tile of the + * tileset. + */ + glm::dmat4x4 HighPrecisionTransform; + + /** + * Updates the voxel renderer based on the newly visible tiles. + * + * @param visibleTiles The visible tiles. + * @param visibleTileScreenSpaceErrors The screen space error values computed + * this frame for the visible tiles. Used to compute priority for voxel tile + * rendering. + */ + void UpdateTiles( + const std::vector& VisibleTiles, + const std::vector& VisibleTileScreenSpaceErrors); + + void UpdateTransformFromCesium(const glm::dmat4& CesiumToUnrealTransform); + +private: + static UMaterialInstanceDynamic* CreateVoxelMaterial( + UCesiumVoxelRendererComponent* pVoxelComponent, + const FVector& dimensions, + const FVector& paddingBefore, + const FVector& paddingAfter, + ACesium3DTileset* pTilesetActor, + const Cesium3DTiles::Class* pVoxelClass, + const FCesiumVoxelClassDescription* pDescription, + const Cesium3DTilesSelection::BoundingVolume& boundingVolume); + + /** + * The resources used to render voxels across the tileset. + */ + TUniquePtr _pResources = nullptr; + + /** + * The tileset that owns this voxel renderer. + */ + ACesium3DTileset* _pTileset = nullptr; +}; diff --git a/Source/CesiumRuntime/Private/CreateGltfOptions.h b/Source/CesiumRuntime/Private/CreateGltfOptions.h index 1d8d0368f..2aab2db1d 100644 --- a/Source/CesiumRuntime/Private/CreateGltfOptions.h +++ b/Source/CesiumRuntime/Private/CreateGltfOptions.h @@ -9,7 +9,9 @@ #include "CesiumGltf/Model.h" #include "CesiumGltf/Node.h" #include "LoadGltfResult.h" +#include "VoxelGridShape.h" +#include #include /** diff --git a/Source/CesiumRuntime/Private/LoadGltfResult.h b/Source/CesiumRuntime/Private/LoadGltfResult.h index 88aafc834..1431b27a7 100644 --- a/Source/CesiumRuntime/Private/LoadGltfResult.h +++ b/Source/CesiumRuntime/Private/LoadGltfResult.h @@ -3,6 +3,8 @@ #pragma once #include "CesiumCommon.h" +#include "CesiumEncodedMetadataUtility.h" +#include "CesiumGltfVoxelComponent.h" #include "CesiumMetadataPrimitive.h" #include "CesiumModelMetadata.h" #include "CesiumPrimitiveFeatures.h" diff --git a/Source/CesiumRuntime/Private/UnrealPrepareRendererResources.cpp b/Source/CesiumRuntime/Private/UnrealPrepareRendererResources.cpp index de6b0c91b..6186c2e37 100644 --- a/Source/CesiumRuntime/Private/UnrealPrepareRendererResources.cpp +++ b/Source/CesiumRuntime/Private/UnrealPrepareRendererResources.cpp @@ -4,8 +4,10 @@ #include "CesiumLifetime.h" #include "CesiumRasterOverlay.h" #include "CesiumRuntime.h" +#include "CesiumVoxelRendererComponent.h" #include "CreateGltfOptions.h" #include "ExtensionImageAssetUnreal.h" + #include #include #include @@ -43,6 +45,10 @@ UnrealPrepareRendererResources::prepareInLoadThread( &(*this->_pActor->_metadataDescription_DEPRECATED); } + if (this->_pActor->_pVoxelRendererComponent) { + options.pVoxelOptions = &this->_pActor->_pVoxelRendererComponent->Options; + } + const CesiumGeospatial::Ellipsoid& ellipsoid = tileLoadResult.ellipsoid; CesiumAsync::Future diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp new file mode 100644 index 000000000..0cf6dde46 --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp @@ -0,0 +1,356 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "VoxelDataTextures.h" + +#include "CesiumGltfVoxelComponent.h" +#include "CesiumMetadataPropertyDetails.h" +#include "CesiumRuntime.h" +#include "CesiumTextureResource.h" +#include "CesiumVoxelMetadataComponent.h" +#include "DataDrivenShaderPlatformInfo.h" +#include "EncodedFeaturesMetadata.h" +#include "EncodedMetadataConversions.h" +#include "Engine/VolumeTexture.h" +#include "Templates/UniquePtr.h" + +#include +#include + +using namespace CesiumGltf; +using namespace EncodedFeaturesMetadata; + +/** + * A Cesium texture resource that creates an initially empty `FRHITexture` for + * FVoxelOctree. + */ +class FCesiumVoxelDataTextureResource : public FCesiumTextureResource { +public: + FCesiumVoxelDataTextureResource( + TextureGroup textureGroup, + uint32 width, + uint32 height, + uint32 depth, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + uint32 extData); + +protected: + virtual FTextureRHIRef InitializeTextureRHI() override; +}; + +FCesiumVoxelDataTextureResource::FCesiumVoxelDataTextureResource( + TextureGroup textureGroup, + uint32 width, + uint32 height, + uint32 depth, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + uint32 extData) + : FCesiumTextureResource( + textureGroup, + width, + height, + depth, + format, + filter, + addressX, + addressY, + sRGB, + false, + extData, + true) {} + +FTextureRHIRef FCesiumVoxelDataTextureResource::InitializeTextureRHI() { + FRHIResourceCreateInfo createInfo{TEXT("FCesiumVoxelDataTextureResource")}; + createInfo.BulkData = nullptr; + createInfo.ExtData = this->_platformExtData; + + ETextureCreateFlags textureFlags = TexCreate_ShaderResource; + if (this->bSRGB) { + textureFlags |= TexCreate_SRGB; + } + + // Create a new 3D RHI texture, initially empty. + return RHICreateTexture( + FRHITextureCreateDesc::Create3D(createInfo.DebugName) + .SetExtent(int32(this->_width), int32(this->_height)) + .SetDepth(this->_depth) + .SetFormat(this->_format) + .SetNumMips(1) + .SetNumSamples(1) + .SetFlags(textureFlags) + .SetInitialState(ERHIAccess::Unknown) + .SetExtData(createInfo.ExtData) + .SetGPUMask(createInfo.GPUMask) + .SetClearValue(createInfo.ClearValueBinding)); +} + +FVoxelDataTextures::FVoxelDataTextures( + const FCesiumVoxelClassDescription* pVoxelClass, + const glm::uvec3& dataDimensions, + ERHIFeatureLevel::Type featureLevel, + uint32 requestedMemoryPerTexture) + : _slots(), + _pEmptySlotsHead(nullptr), + _pOccupiedSlotsHead(nullptr), + _dataDimensions(dataDimensions), + _tileCountAlongAxes(0), + _maximumTileCount(0), + _propertyMap() { + if (!RHISupportsVolumeTextures(featureLevel)) { + // TODO: 2D fallback? + UE_LOG( + LogCesium, + Error, + TEXT( + "Volume textures are not supported. Unable to create the textures necessary for rendering voxels.")) + return; + } + + if (!pVoxelClass) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Voxel tileset is missing a UCesiumVoxelMetadataComponent. Add a UCesiumVoxelMetadataComponent to visualize the metadata within the tileset.")) + return; + } + + if (pVoxelClass->Properties.IsEmpty()) { + return; + } + + // Attributes can take up varying texel sizes based on their type. + // So first, identify which attribute is the largest in size. + uint32 maximumTexelSizeBytes = 0; + for (const FCesiumPropertyAttributePropertyDescription& Property : + pVoxelClass->Properties) { + EncodedPixelFormat encodedFormat = getPixelFormat( + Property.EncodingDetails.Type, + Property.EncodingDetails.ComponentType); + if (encodedFormat.format == EPixelFormat::PF_Unknown) { + continue; + } + + uint32 texelSizeBytes = + encodedFormat.channels * encodedFormat.bytesPerChannel; + this->_propertyMap.Add(Property.Name, {encodedFormat, nullptr, nullptr}); + maximumTexelSizeBytes = FMath::Max(maximumTexelSizeBytes, texelSizeBytes); + } + + if (maximumTexelSizeBytes == 0) { + UE_LOG( + LogCesium, + Error, + TEXT( + "No properties on UCesiumVoxelMetadataComponent are valid; none will be passed to the material.")) + return; + } + + uint32 texelCount = requestedMemoryPerTexture / maximumTexelSizeBytes; + uint32 textureDimension = std::cbrtf(static_cast(texelCount)); + + this->_tileCountAlongAxes = + glm::uvec3(textureDimension) / this->_dataDimensions; + + if (this->_tileCountAlongAxes.x == 0 || this->_tileCountAlongAxes.y == 0 || + this->_tileCountAlongAxes.z == 0) { + UE_LOG( + LogCesium, + Error, + TEXT( + "Unable to create data textures for voxel dataset due to limited memory.")) + return; + } + + glm::uvec3 actualDimensions = + this->_tileCountAlongAxes * this->_dataDimensions; + + this->_maximumTileCount = this->_tileCountAlongAxes.x * + this->_tileCountAlongAxes.y * + this->_tileCountAlongAxes.z; + + // Initialize the data slots. + this->_slots.resize(this->_maximumTileCount, Slot()); + for (size_t i = 0; i < this->_slots.size(); i++) { + Slot* pSlot = &_slots[i]; + pSlot->Index = static_cast(i); + pSlot->Previous = i > 0 ? &_slots[i - 1] : nullptr; + pSlot->Next = i < _slots.size() - 1 ? &_slots[i + 1] : nullptr; + } + + this->_pEmptySlotsHead = &this->_slots[0]; + + // Create the actual textures. + for (auto& propertyIt : this->_propertyMap) { + FCesiumTextureResource* pTextureResource = + MakeUnique( + TextureGroup::TEXTUREGROUP_8BitData, + actualDimensions.x, + actualDimensions.y, + actualDimensions.z, + propertyIt.Value.encodedFormat.format, + TextureFilter::TF_Nearest, + TextureAddress::TA_Clamp, + TextureAddress::TA_Clamp, + false, + 0) + .Release(); + + UVolumeTexture* pTexture = NewObject( + GetTransientPackage(), + MakeUniqueObjectName( + GetTransientPackage(), + UTexture2D::StaticClass(), + "CesiumVoxelDataTexture"), + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + pTexture->Filter = TextureFilter::TF_Nearest; + pTexture->LODGroup = TextureGroup::TEXTUREGROUP_8BitData; + pTexture->SRGB = false; + pTexture->NeverStream = true; + + pTexture->SetResource(pTextureResource); + + propertyIt.Value.pTexture = pTexture; + propertyIt.Value.pResource = pTextureResource; + + ENQUEUE_RENDER_COMMAND(Cesium_InitResource) + ([pTexture, + pResource = pTextureResource](FRHICommandListImmediate& RHICmdList) { + pResource->SetTextureReference( + pTexture->TextureReference.TextureReferenceRHI); + pResource->InitResource(FRHICommandListImmediate::Get()); + }); + } +} + +/** + * NOTE: This function assumes that the data being read from pData is the same + * type that the texture expects. Coercive encoding behavior (similar to what is + * done for CesiumPropertyTableProperty) could be added in the future. + */ +static void writeTo3DTexture( + FCesiumTextureResource* pResource, + const std::byte* pData, + FUpdateTextureRegion3D updateRegion, + uint32 texelSizeBytes) { + if (!pResource || !pData) + return; + + ENQUEUE_RENDER_COMMAND(Cesium_CopyVoxels) + ([pResource, pData, updateRegion, texelSizeBytes]( + FRHICommandListImmediate& RHICmdList) { + // Pitch = size in bytes of each row of the source image. + uint32 srcRowPitch = updateRegion.Width * texelSizeBytes; + uint32 srcDepthPitch = + updateRegion.Width * updateRegion.Height * texelSizeBytes; + + RHIUpdateTexture3D( + pResource->TextureRHI, + 0, + updateRegion, + srcRowPitch, + srcDepthPitch, + (const uint8*)(pData)); + }); +} + +int64 FVoxelDataTextures::Add(const UCesiumGltfVoxelComponent& voxelComponent) { + uint32 slotIndex = ReserveNextSlot(); + if (slotIndex < 0) { + return -1; + } + + // Compute the update region for the data textures. + FUpdateTextureRegion3D updateRegion; + updateRegion.Width = this->_dataDimensions.x; + updateRegion.Height = this->_dataDimensions.y; + updateRegion.Depth = this->_dataDimensions.z; + updateRegion.SrcX = 0; + updateRegion.SrcY = 0; + updateRegion.SrcZ = 0; + + uint32 zSlice = this->_tileCountAlongAxes.x * this->_tileCountAlongAxes.y; + uint32 indexZ = slotIndex / zSlice; + uint32 indexY = (slotIndex % zSlice) / this->_tileCountAlongAxes.x; + uint32 indexX = slotIndex % this->_tileCountAlongAxes.x; + updateRegion.DestZ = indexZ * this->_dataDimensions.z; + updateRegion.DestY = indexY * this->_dataDimensions.y; + updateRegion.DestX = indexX * this->_dataDimensions.x; + + uint32 index = static_cast(slotIndex); + + for (auto PropertyIt : this->_propertyMap) { + const ValidatedVoxelBuffer* pValidBuffer = + voxelComponent.attributeBuffers.Find(PropertyIt.Key); + if (!pValidBuffer) { + continue; + } + + uint32 texelSizeBytes = PropertyIt.Value.encodedFormat.bytesPerChannel * + PropertyIt.Value.encodedFormat.channels; + const std::byte* pData = pValidBuffer->pBuffer->cesium.data.data(); + pData += pValidBuffer->pBufferView->byteOffset; + + writeTo3DTexture( + PropertyIt.Value.pResource, + pData, + updateRegion, + texelSizeBytes); + } + return slotIndex; +} + +int64 FVoxelDataTextures::ReserveNextSlot() { + // Remove head from list of empty slots + FVoxelDataTextures::Slot* pSlot = this->_pEmptySlotsHead; + if (!pSlot) { + return -1; + } + + this->_pEmptySlotsHead = pSlot->Next; + + if (this->_pEmptySlotsHead) { + this->_pEmptySlotsHead->Previous = nullptr; + } + + // Move to list of occupied slots (as the new head) + pSlot->Next = this->_pOccupiedSlotsHead; + if (pSlot->Next) { + this->_pOccupiedSlotsHead->Previous = pSlot; + } + this->_pOccupiedSlotsHead = pSlot; + + return pSlot->Index; +} + +bool FVoxelDataTextures::Release(uint32 slotIndex) { + if (slotIndex >= this->_slots.size()) { + return false; // Index out of bounds + } + + Slot* pSlot = &this->_slots[slotIndex]; + if (pSlot->Previous) { + pSlot->Previous->Next = pSlot->Next; + } + if (pSlot->Next) { + pSlot->Next->Previous = pSlot->Previous; + } + + // Move to list of empty slots (as the new head) + pSlot->Next = this->_pEmptySlotsHead; + if (pSlot->Next) { + pSlot->Next->Previous = pSlot; + } + + pSlot->Previous = nullptr; + this->_pEmptySlotsHead = pSlot; + + return true; +} diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.h b/Source/CesiumRuntime/Private/VoxelDataTextures.h new file mode 100644 index 000000000..5c6b8058b --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.h @@ -0,0 +1,146 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumCommon.h" +#include "CesiumMetadataValueType.h" +#include "EncodedFeaturesMetadata.h" +#include + +#include + +namespace Cesium3DTiles { +struct Class; +} + +struct FCesiumVoxelClassDescription; +class FCesiumTextureResource; +class UCesiumGltfVoxelComponent; + +/** + * Manages the data texture resources for a voxel dataset, where + * each data texture represents an attribute. This is responsible for + * synchronizing which slots are occupied across all data textures. + * + * Due to the requirements of voxel rendering (primarily, sampling voxels from + * neighboring tiles), the voxels within a tileset are drawn in a single pass. + * This texture manages all of the currently-loaded voxel data and is itself + * passed to the material. + * + * Counterpart to Megatexture.js in CesiumJS, except this takes advantage of 3D + * textures to simplify some of the texture read/write math. + */ +class FVoxelDataTextures { +public: + /** + * @brief Constructs a set of voxel data textures. + * + * @param pVoxelClass The voxel class description, indicating which metadata + * attributes to encode. + * @param dataDimensions The dimensions of the voxel data, including padding. + * @param featureLevel The RHI feature level associated with the scene. + * @param requestedMemoryPerTexture The requested texture memory for each + * voxel attribute. + */ + FVoxelDataTextures( + const FCesiumVoxelClassDescription* pVoxelClass, + const glm::uvec3& dataDimensions, + ERHIFeatureLevel::Type featureLevel, + uint32 requestedMemoryPerTexture); + + /** + * Gets the maximum number of tiles that can be added to the data + * textures. Equivalent to the maximum number of data slots. + */ + uint32 GetMaximumTileCount() const { + return static_cast(this->_slots.size()); + } + + /** + * Gets the number of tiles along each dimension of the textures. + */ + glm::uvec3 GetTileCountAlongAxes() const { return this->_tileCountAlongAxes; } + + /** + * Retrieves the texture containing the data for the attribute with + * the given ID. Returns nullptr if the attribute does not exist. + */ + UTexture* GetDataTexture(const FString& attributeId) const { + const Property* pProperty = this->_propertyMap.Find(attributeId); + if (pProperty) { + return pProperty->pTexture; + } + return nullptr; + } + + /** + * @brief Retrieves how many data textures exist. + */ + int32 GetTextureCount() const { return this->_propertyMap.Num(); } + + /** + * Whether or not all slots in the textures are occupied. + */ + bool IsFull() const { return this->_pEmptySlotsHead == nullptr; } + + /** + * Attempts to add the voxel tile to the data textures. + * + * @returns The index of the reserved slot, or -1 if none were available. + */ + int64_t Add(const UCesiumGltfVoxelComponent& voxelComponent); + + /** + * Reserves the next available empty slot. + * + * @returns The index of the reserved slot, or -1 if none were available. + */ + int64 ReserveNextSlot(); + + /** + * Releases the slot at the specified index, making the space available for + * another voxel tile. + */ + bool Release(uint32 slotIndex); + +private: + /** + * Represents a slot in the voxel data texture that contains a single + * tile's data. Slots function like nodes in a linked list in order to track + * which slots are occupied with data, while preventing the need for 2 vectors + * with maximum tile capacity. + */ + struct Slot { + int64 Index = -1; + Slot* Next = nullptr; + Slot* Previous = nullptr; + }; + + struct Property { + /** + * The texture format used to store encoded property values. + */ + EncodedFeaturesMetadata::EncodedPixelFormat encodedFormat; + + // TODO: have to check RHISupportsVolumeTextures(GetFeatureLevel()) + // not sure if same as SupportsVolumeTextureRendering, which is false on + // Vulkan Android, Metal, and OpenGL + UTexture* pTexture; + + /** + * A pointer to the texture resource. There is no way to retrieve this + * through the UTexture API, so the pointer is stored here. + */ + FCesiumTextureResource* pResource; + }; + + std::vector _slots; + Slot* _pEmptySlotsHead; + Slot* _pOccupiedSlotsHead; + + glm::uvec3 _dataDimensions; + glm::uvec3 _tileCountAlongAxes; + uint32 _maximumTileCount; + + TMap _propertyMap; +}; diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp new file mode 100644 index 000000000..deb5ddd04 --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -0,0 +1,487 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "VoxelOctree.h" +#include "CesiumRuntime.h" + +#include +#include + +using namespace CesiumGeometry; +using namespace Cesium3DTilesContent; + +/** + * A Cesium texture resource that creates an initially empty `FRHITexture` for + * FVoxelOctree. + */ +class FCesiumVoxelOctreeTextureResource : public FCesiumTextureResource { +public: + FCesiumVoxelOctreeTextureResource( + TextureGroup textureGroup, + uint32 width, + uint32 height, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + uint32 extData); + +protected: + virtual FTextureRHIRef InitializeTextureRHI() override; +}; + +FCesiumVoxelOctreeTextureResource::FCesiumVoxelOctreeTextureResource( + TextureGroup textureGroup, + uint32 width, + uint32 height, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + uint32 extData) + : FCesiumTextureResource( + textureGroup, + width, + height, + 0, + format, + filter, + addressX, + addressY, + sRGB, + false, + extData, + true) {} + +FTextureRHIRef FCesiumVoxelOctreeTextureResource::InitializeTextureRHI() { + FRHIResourceCreateInfo createInfo{TEXT("FVoxelOctreeTextureResource")}; + createInfo.BulkData = nullptr; + createInfo.ExtData = this->_platformExtData; + + ETextureCreateFlags textureFlags = TexCreate_ShaderResource; + if (this->bSRGB) { + textureFlags |= TexCreate_SRGB; + } + + // Create a new 2D RHI texture, initially empty. + return RHICreateTexture( + FRHITextureCreateDesc::Create2D(createInfo.DebugName) + .SetExtent(int32(this->_width), int32(this->_height)) + .SetFormat(this->_format) + .SetNumMips(1) + .SetNumSamples(1) + .SetFlags(textureFlags) + .SetInitialState(ERHIAccess::Unknown) + .SetExtData(createInfo.ExtData) + .SetGPUMask(createInfo.GPUMask) + .SetClearValue(createInfo.ClearValueBinding)); +} + +void UCesiumVoxelOctreeTexture::Update(const std::vector& data) { + if (!this->_pResource || !this->_pResource->TextureRHI) { + return; + } + + // Compute the area of the texture that actually needs updating. + uint32 texelCount = data.size() / sizeof(uint32); + uint32 tileCount = texelCount / TexelsPerNode; + + glm::uvec2 updateExtent; + if (tileCount <= this->_tilesPerRow) { + updateExtent.x = texelCount; + updateExtent.y = 1; + } else { + updateExtent.x = this->_tilesPerRow * TexelsPerNode; + updateExtent.y = texelCount / updateExtent.x; + } + updateExtent = glm::max(updateExtent, glm::uvec2(1, 1)); + + FUpdateTextureRegion2D region; + region.DestX = 0; + region.DestY = 0; + region.Width = updateExtent.x; + region.Height = updateExtent.y; + region.SrcX = 0; + region.SrcY = 0; + + region.Width = FMath::Clamp(region.Width, 1, this->_pResource->GetSizeX()); + region.Height = FMath::Clamp(region.Height, 1, this->_pResource->GetSizeY()); + + // Pitch = size in bytes of each row of the source image + uint32 sourcePitch = region.Width * sizeof(uint32); + ENQUEUE_RENDER_COMMAND(Cesium_UpdateResource) + ([pResource = this->_pResource, &data, region, sourcePitch]( + FRHICommandListImmediate& RHICmdList) { + RHIUpdateTexture2D( + pResource->TextureRHI, + 0, + region, + sourcePitch, + reinterpret_cast(data.data())); + }); +} + +/*static*/ UCesiumVoxelOctreeTexture* +UCesiumVoxelOctreeTexture::Create(uint32 Width, uint32 MaximumTileCount) { + uint32 TilesPerRow = Width / TexelsPerNode; + float Height = (float)MaximumTileCount / (float)TilesPerRow; + Height = static_cast(FMath::CeilToInt64(Height)); + Height = FMath::Clamp(Height, 1, Width); + + TUniquePtr pResource = + MakeUnique( + TextureGroup::TEXTUREGROUP_8BitData, + Width, + Height, + EPixelFormat::PF_R8G8B8A8, + TextureFilter::TF_Nearest, + TextureAddress::TA_Clamp, + TextureAddress::TA_Clamp, + false, + 0); + + UCesiumVoxelOctreeTexture* pTexture = NewObject( + GetTransientPackage(), + MakeUniqueObjectName( + GetTransientPackage(), + UTexture2D::StaticClass(), + "VoxelOctreeTexture"), + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + + pTexture->AddressX = TextureAddress::TA_Clamp; + pTexture->AddressY = TextureAddress::TA_Clamp; + pTexture->Filter = TextureFilter::TF_Nearest; + pTexture->LODGroup = TextureGroup::TEXTUREGROUP_8BitData; + pTexture->SRGB = false; + pTexture->NeverStream = true; + + pTexture->_tilesPerRow = TilesPerRow; + pTexture->_pResource = pResource.Release(); + pTexture->SetResource(pTexture->_pResource); + + return pTexture; +} + +size_t FVoxelOctree::OctreeTileIDHash::operator()( + const CesiumGeometry::OctreeTileID& tileId) const { + // Tiles with the same morton index, but on different levels, are + // distinguished by an offset. This offset is equal to the number of tiles on + // the levels above it, which is the sum of a series, where n = tile.level - + // 1: + // 1 + 8 + 8^2 + ... + 8^n = (8^(n+1) - 1) / (8 - 1) + // e.g., for TileID(2, 0, 0, 0), the morton index is 0, but the hash = 9. + size_t levelOffset = + tileId.level > 0 ? (std::pow(8, tileId.level) - 1) / 7 : 0; + return levelOffset + ImplicitTilingUtilities::computeMortonIndex(tileId); +} + +FVoxelOctree::FVoxelOctree() : _nodes() { + CesiumGeometry::OctreeTileID rootTileID(0, 0, 0, 0); + this->_nodes.insert({rootTileID, Node()}); +} + +FVoxelOctree::~FVoxelOctree() { + std::vector empty; + std::swap(this->_octreeData, empty); + + // Can we count on this being freed since it's a UTexture? + this->_pTexture = nullptr; +} + +void FVoxelOctree::InitializeTexture(uint32 Width, uint32 MaximumTileCount) { + this->_pTexture = UCesiumVoxelOctreeTexture::Create(Width, MaximumTileCount); + FTextureResource* pResource = + this->_pTexture ? this->_pTexture->GetResource() : nullptr; + if (!pResource) { + UE_LOG( + LogCesium, + Error, + TEXT("Could not create texture for voxel octree.")); + return; + } + + ENQUEUE_RENDER_COMMAND(Cesium_InitResource) + ([pTexture = this->_pTexture, + pResource](FRHICommandListImmediate& RHICmdList) { + pResource->SetTextureReference( + pTexture->TextureReference.TextureReferenceRHI); + pResource->InitResource( + FRHICommandListImmediate::Get()); // Init Resource now requires a + // command list. + }); +} + +FVoxelOctree::Node* +FVoxelOctree::GetNode(const CesiumGeometry::OctreeTileID& TileID) const { + if (this->_nodes.find(TileID) != this->_nodes.end()) { + return const_cast(&this->_nodes.at(TileID)); + } + return nullptr; +} + +bool FVoxelOctree::CreateNode(const CesiumGeometry::OctreeTileID& TileID) { + FVoxelOctree::Node* pNode = this->GetNode(TileID); + if (pNode) { + return false; + } + + // Create this node first. + this->_nodes.insert({TileID, Node()}); + pNode = &this->_nodes[TileID]; + + // Starting from the target node, traverse the tree upwards and create the + // missing nodes. Stop when we've found an existing parent node. + CesiumGeometry::OctreeTileID currentTileID = TileID; + bool foundExistingParent = false; + for (uint32_t level = TileID.level; level > 0; level--) { + CesiumGeometry::OctreeTileID parentTileID = + *ComputeParentTileID(currentTileID); + if (this->_nodes.find(parentTileID) == this->_nodes.end()) { + // Parent doesn't exist, so create it. + this->_nodes.insert({parentTileID, Node()}); + } else { + foundExistingParent = true; + } + + FVoxelOctree::Node* pParent = &this->_nodes[parentTileID]; + pNode->Parent = pParent; + + // The parent *shouldn't* have children at this point. Otherwise, our + // target node would have already been found. + std::array childIds = + ComputeChildTileIDs(parentTileID); + for (CesiumGeometry::OctreeTileID& childId : childIds) { + if (this->_nodes.find(childId) == this->_nodes.end()) { + this->_nodes.insert({childId, Node()}); + this->_nodes[childId].Parent = pParent; + } + } + + pParent->HasChildren = true; + if (foundExistingParent) { + // The parent already existed in the tree previously, no need to create + // its ancestors. + break; + } + + currentTileID = parentTileID; + pNode = pParent; + } + + return true; +} + +bool FVoxelOctree::RemoveNode(const CesiumGeometry::OctreeTileID& TileID) { + if (TileID.level == 0) { + return false; + } + + if (IsNodeRenderable(TileID)) { + return false; + } + + // Check the sibling nodes. If they are either leaves or have renderable + // children, return true. + bool hasRenderableSiblings = false; + + // There may be cases where the children rely on the parent for rendering. + // If so, the node's data cannot be easily released. + // TODO: can you also attempt to destroy the node? + CesiumGeometry::OctreeTileID parentTileID = *ComputeParentTileID(TileID); + std::array siblingIds = + ComputeChildTileIDs(parentTileID); + for (const CesiumGeometry::OctreeTileID& siblingId : siblingIds) { + if (siblingId == TileID) + continue; + + if (IsNodeRenderable(siblingId)) { + hasRenderableSiblings = true; + break; + } + } + + if (hasRenderableSiblings) { + // Don't remove this node yet. It will have to rely on its parent for + // rendering. + return false; + } + + // Otherwise, okay to remove the nodes. + for (const CesiumGeometry::OctreeTileID& siblingId : siblingIds) { + this->_nodes.erase(this->_nodes.find(siblingId)); + } + this->GetNode(parentTileID)->HasChildren = false; + + // Continue to recursively remove parent nodes as long as they aren't + // renderable either. + RemoveNode(parentTileID); + + return true; +} + +bool FVoxelOctree::IsNodeRenderable( + const CesiumGeometry::OctreeTileID& TileID) const { + FVoxelOctree::Node* pNode = this->GetNode(TileID); + if (!pNode) { + return false; + } + + return pNode->DataSlotIndex >= 0 || pNode->HasChildren; +} + +/*static*/ std::array +FVoxelOctree::ComputeChildTileIDs(const CesiumGeometry::OctreeTileID& TileID) { + uint32 level = TileID.level + 1; + uint32 x = TileID.x << 1; + uint32 y = TileID.y << 1; + uint32 z = TileID.z << 1; + + return { + OctreeTileID(level, x, y, z), + OctreeTileID(level, x + 1, y, z), + OctreeTileID(level, x, y + 1, z), + OctreeTileID(level, x + 1, y + 1, z), + OctreeTileID(level, x, y, z + 1), + OctreeTileID(level, x + 1, y, z + 1), + OctreeTileID(level, x, y + 1, z + 1), + OctreeTileID(level, x + 1, y + 1, z + 1)}; +} + +/*static*/ std::optional +FVoxelOctree::ComputeParentTileID(const CesiumGeometry::OctreeTileID& TileID) { + if (TileID.level == 0) { + return std::nullopt; + } + return CesiumGeometry::OctreeTileID( + TileID.level - 1, + TileID.x >> 1, + TileID.y >> 1, + TileID.z >> 1); +} + +/*static*/ void FVoxelOctree::insertNodeData( + std::vector& nodeData, + uint32 textureIndex, + FVoxelOctree::ENodeFlag nodeFlag, + uint16 data, + uint8 renderableLevelDifference) { + uint32 dataIndex = textureIndex * sizeof(uint32); + if (nodeData.size() <= dataIndex) { + nodeData.resize(dataIndex + sizeof(uint32), std::byte(0)); + } + // Explicitly encode the values in little endian order. + nodeData[dataIndex] = std::byte(nodeFlag); + nodeData[dataIndex + 1] = std::byte(renderableLevelDifference); + nodeData[dataIndex + 2] = std::byte(data & 0x00ff); + nodeData[dataIndex + 3] = std::byte(data >> 8); +}; + +void FVoxelOctree::encodeNode( + const CesiumGeometry::OctreeTileID& tileId, + std::vector& nodeData, + uint32& nodeCount, + uint32 octreeIndex, + uint32 textureIndex, + uint32 parentOctreeIndex, + uint32 parentTextureIndex) { + constexpr uint32 TexelsPerNode = UCesiumVoxelOctreeTexture::TexelsPerNode; + + Node* pNode = &this->_nodes[tileId]; + if (pNode->HasChildren) { + // Point the parent and child octree indices at each other + insertNodeData( + nodeData, + parentTextureIndex, + ENodeFlag::Internal, + octreeIndex); + insertNodeData( + nodeData, + textureIndex, + ENodeFlag::Internal, + parentOctreeIndex); + nodeCount++; + + // Continue traversing + parentOctreeIndex = octreeIndex; + parentTextureIndex = parentOctreeIndex * TexelsPerNode + 1; + + std::array childIds = + ComputeChildTileIDs(tileId); + for (uint32 i = 0; i < childIds.size(); i++) { + octreeIndex = nodeCount; + textureIndex = octreeIndex * TexelsPerNode; + + encodeNode( + childIds[i], + nodeData, + nodeCount, + octreeIndex, + textureIndex, + parentOctreeIndex, + parentTextureIndex + i); + } + } else { + // Leaf nodes involve more complexity. + ENodeFlag flag = ENodeFlag::Empty; + uint16 value = 0; + uint16 levelDifference = 0; + + if (pNode->DataSlotIndex >= 0) { + flag = ENodeFlag::Leaf; + value = static_cast(pNode->DataSlotIndex); + } else if (pNode->Parent) { + FVoxelOctree::Node* pParent = pNode->Parent; + + for (uint32 levelsAbove = 1; levelsAbove <= tileId.level; levelsAbove++) { + if (pParent->DataSlotIndex >= 0) { + flag = ENodeFlag::Leaf; + value = static_cast(pParent->DataSlotIndex); + levelDifference = levelsAbove; + break; + } + + // Continue trying to find a renderable ancestor. + pParent = pParent->Parent; + + if (!pParent) { + // This happens if we've reached the root node and it's not + // renderable. + break; + } + } + } + insertNodeData(nodeData, parentTextureIndex, flag, value, levelDifference); + nodeCount++; + } +} + +void FVoxelOctree::UpdateTexture() { + if (!this->_pTexture) { + return; + } + + this->_octreeData.clear(); + uint32_t nodeCount = 0; + encodeNode( + CesiumGeometry::OctreeTileID(0, 0, 0, 0), + this->_octreeData, + nodeCount, + 0, + 0, + 0, + 0); + + // Pad the data as necessary for the texture copy. + uint32 regionWidth = this->_pTexture->GetTilesPerRow() * + UCesiumVoxelOctreeTexture::TexelsPerNode * + sizeof(uint32); + uint32 regionHeight = + glm::ceil((float)this->_octreeData.size() / regionWidth); + uint32 expectedSize = regionWidth * regionHeight; + if (this->_octreeData.size() != expectedSize) { + this->_octreeData.resize(expectedSize, std::byte(0)); + } + + this->_pTexture->Update(this->_octreeData); +} diff --git a/Source/CesiumRuntime/Private/VoxelOctree.h b/Source/CesiumRuntime/Private/VoxelOctree.h new file mode 100644 index 000000000..7a3642c95 --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelOctree.h @@ -0,0 +1,256 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumTextureResource.h" +#include "Engine/Texture2D.h" + +#include +#include +#include +#include + +class UCesiumVoxelOctreeTexture : public UTexture2D { +public: + /** + * @brief Constant representing the number of texels used to represent a node + * in the octree texture. + * + * The first texel is used to store an index to the node's parent. The + * remaining eight represent the indices of the node's children. + */ + static const uint32 TexelsPerNode = 9; + + static UCesiumVoxelOctreeTexture* + Create(uint32 Width, uint32 MaximumTileCount); + + /** + * Updates the octree texture with the new input data. + */ + void Update(const std::vector& data); + + uint32_t GetTilesPerRow() const { return this->_tilesPerRow; } + +private: + FCesiumTextureResource* _pResource; + uint32_t _tilesPerRow; +}; + +/** + * @brief A representation of an implicitly tiled octree containing voxel + * values. + */ +class FVoxelOctree { +public: + /** + * @brief A representation of a tile in an implicitly tiled octree. + */ + struct Node { + bool HasChildren; + double LastKnownScreenSpaceError; + int64_t DataSlotIndex; + Node* Parent; + + Node() + : HasChildren(false), + LastKnownScreenSpaceError(0.0), + DataSlotIndex(-1), + Parent(nullptr) {} + }; + + /** + * @brief An enum that indicates the type of a node encoded on the GPU. + * Indicates what the numerical data value represents for that node. + */ + enum class ENodeFlag : uint8 { + /** + * Empty leaf node that should be skipped when rendering. + * + * This may happen if a node's sibling is renderable, but neither it nor its + * parent are renderable, which can happen Native's algorithm loads higher + * LOD tiles before their ancestors. + */ + Empty = 0, + /** + * Renderable leaf node with two possibilities: + * + * 1. The leaf node has its own data. The encoded data value refers to an + * index in the data texture of the slot containing the voxel tile's data. + * + * 2. The leaf node has no data of its own but is forced to render (such as + * when its siblings are renderable but it is not). The leaf will attempt to + * render the data of the nearest ancestor. The encoded data value refers to + * an index in the data texture of the slot containing the ancestor voxel + * tile's data. + * + * The latter is a unique case that contains an extra packed value -- the + * level difference from the nearest renderable ancestor. This is so the + * rendering implementation can deduce the correct texture coordinates. If + * the leaf node contains its own data, then this value is 0. + */ + Leaf = 1, + /** + * Internal node. The encoded data value refers to an index in the octree + * texture where its full representation is located. + */ + Internal = 2, + }; + + FVoxelOctree(); + ~FVoxelOctree(); + + /** + * @brief Gets a node in the octree at the specified tile ID. Returns nullptr + * if it does not exist. + * + * @param TileID The octree tile ID. + */ + Node* GetNode(const CesiumGeometry::OctreeTileID& TileID) const; + + /** + * @brief Creates a node in the octree at the specified tile ID, including the + * parent nodes needed to traverse to it. + * + * If the node already exists, this returns false. + * + * @param TileID The octree tile ID. + * @param Whether the node was successfully added. + */ + bool CreateNode(const CesiumGeometry::OctreeTileID& TileID); + + /** + * @brief Attempts to remove the node at the specified tile ID. + * + * This will fail to remove the node from the tree if: + * - the node is the root of the tree + * - the node has renderable siblings + * + * @param TileID The octree tile ID. + * @return Whether the node was successfully removed. + */ + bool RemoveNode(const CesiumGeometry::OctreeTileID& TileID); + + bool IsNodeRenderable(const CesiumGeometry::OctreeTileID& TileID) const; + + /** + * @brief Retrieves the tile IDs for the children of the given tile. Does not + * validate whether these exist in the octree. + */ + static std::array + ComputeChildTileIDs(const CesiumGeometry::OctreeTileID& TileID); + + /** + * @brief Retrieves the tile ID for the parent of the given tile. Does not + * validate whether either tile exists in the octree. + * + * @returns The parent tile ID, or std::nullopt if the given tile is a root + * tile (i.e., level 0). + */ + static std::optional + ComputeParentTileID(const CesiumGeometry::OctreeTileID& TileID); + + void InitializeTexture(uint32 Width, uint32 MaximumTileCount); + + /** + * @brief Retrieves the texture containing the encoded octree. + */ + UTexture2D* GetTexture() const { return this->_pTexture; } + + void UpdateTexture(); + +private: + /** + * @brief Inserts the input values to the data vector, automatically + * expanding it if the target index is out-of-bounds. + */ + static void insertNodeData( + std::vector& nodeData, + uint32 textureIndex, + ENodeFlag nodeFlag, + uint16 data, + uint8 renderableLevelDifference = 0); + + /** + * @brief Recursively writes octree nodes as their expected representation + * in the GPU texture. + * + * Example Below (shown as binary tree instead of octree for + * demonstration purposes) + * + * Tree: + * 0 + * / \ + * / \ + * / \ + * 1 3 + * / \ / \ + * L0 2 L3 L4 + * / \ + * L1 L2 + * + * + * GPU Array: + * L = leaf index + * * = index to parent node + * node index: 0_______ 1________ 2________ 3_________ + * data array: [*0, 1, 3, *0, L0, 2, *1 L1, L2, *0, L3, L4] + * + * The array is generated from a depth-first traversal. The end result could + * be an unbalanced tree, so the parent index is stored at each node to make + * it possible to traverse upwards. + * + * Nodes are indexed by the order in which they appear in the traversal. + * + * @param pNode The node to be encoded. + * @param nodeData The data buffer to write to. + * @param nodeCount The current number of encoded numbers, used to assign + * indices to each node. Accumulates over all calls of this function. + * @param octreeIndex The index of the node relative to all the nodes + * encountered in the octree, based on nodeCount. Acts as the identifier for + * the node. + * @param textureIndex The texel index of the node within the texture where + * the node will be expanded if it is an internal node. + * @param parentOctreeIndex The octree index of the parent. + * @param parentTextureIndex The texel index of the node within the texture + * where the node data will be written, relative to the parent's texel + * index. + */ + void encodeNode( + const CesiumGeometry::OctreeTileID& tileId, + std::vector& nodeData, + uint32& nodeCount, + uint32 octreeIndex, + uint32 textureIndex, + uint32 parentOctreeIndex, + uint32 parentTextureIndex); + + // This implementation is inspired by Linear (hashed) Octrees: + // https://geidav.wordpress.com/2014/08/18/advanced-octrees-2-node-representations/ + // + // First, nodes must track their parent / child relationships so that the tree + // structure can be encoded to a texture, for voxel raymarching. + // + // Second, nodes must be easily created and/or accessed. cesium-native passes + // tiles in over in a vector without spatial organization. Typical tree + // queries are O(log(n)) where n = # tree levels. This is unideal, since it's + // likely that multiple tiles will be made visible in an update based on + // movement. + // + // The compromise: a hashmap that stores octree nodes based on their tile ID. + // The nodes don't point to any children themselves; instead, they store a + // bool indicating whether or not children have been created for them. It's up + // to the octree to properly manage this. + struct OctreeTileIDHash { + size_t operator()(const CesiumGeometry::OctreeTileID& tileId) const; + }; + + using NodeMap = + std::unordered_map; + NodeMap _nodes; + + UCesiumVoxelOctreeTexture* _pTexture; + + // As the octree grows, save the allocated memory so that recomputing the + // same-size octree won't require more allocations. + std::vector _octreeData; +}; diff --git a/Source/CesiumRuntime/Private/VoxelResources.cpp b/Source/CesiumRuntime/Private/VoxelResources.cpp new file mode 100644 index 000000000..76e10af60 --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelResources.cpp @@ -0,0 +1,228 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "VoxelResources.h" +#include "CesiumGltfComponent.h" +#include "CesiumRuntime.h" +#include "EncodedMetadataConversions.h" +#include "VoxelGridShape.h" + +#include +#include +#include +#include +#include + +#include +#include + +using namespace CesiumGltf; +using namespace Cesium3DTilesContent; + +FVoxelResources::FVoxelResources( + const FCesiumVoxelClassDescription* pVoxelClass, + EVoxelGridShape shape, + const glm::uvec3& dataDimensions, + ERHIFeatureLevel::Type featureLevel, + uint32 requestedMemoryPerDataTexture) + : _dataTextures( + pVoxelClass, + dataDimensions, + featureLevel, + requestedMemoryPerDataTexture), + _loadedNodeIds(), + _visibleTileQueue() { + uint32 width = MaximumOctreeTextureWidth; + uint32 maximumTileCount = this->_dataTextures.GetMaximumTileCount(); + this->_octree.InitializeTexture(width, maximumTileCount); + this->_loadedNodeIds.reserve(maximumTileCount); +} + +FVoxelResources::~FVoxelResources() { + // TODO cleanup? +} + +FVector FVoxelResources::GetTileCount() const { + auto tileCount = this->_dataTextures.GetTileCountAlongAxes(); + return FVector(tileCount.x, tileCount.y, tileCount.z); +} + +namespace { +template +void forEachRenderableVoxelTile(const auto& tiles, Func&& f) { + for (size_t i = 0; i < tiles.size(); i++) { + Cesium3DTilesSelection::Tile* pTile = tiles[i]; + if (!pTile || + pTile->getState() != Cesium3DTilesSelection::TileLoadState::Done) { + continue; + } + + const Cesium3DTilesSelection::TileContent& content = pTile->getContent(); + const Cesium3DTilesSelection::TileRenderContent* pRenderContent = + content.getRenderContent(); + if (!pRenderContent) { + continue; + } + + UCesiumGltfComponent* Gltf = static_cast( + pRenderContent->getRenderResources()); + if (!Gltf) { + // When a tile does not have render resources (i.e. a glTF), then + // the resources either have not yet been loaded or prepared, + // or the tile is from an external tileset and does not directly + // own renderable content. In both cases, the tile is ignored here. + continue; + } + + const TArray& Children = Gltf->GetAttachChildren(); + for (USceneComponent* pChild : Children) { + UCesiumGltfVoxelComponent* pVoxelComponent = + Cast(pChild); + if (!pVoxelComponent || pVoxelComponent->attributeBuffers.IsEmpty()) { + continue; + } + + f(i, pTile, pVoxelComponent); + } + } +} +} // namespace + +void FVoxelResources::Update( + const std::vector& VisibleTiles, + const std::vector& VisibleTileScreenSpaceErrors) { + forEachRenderableVoxelTile( + VisibleTiles, + [&VisibleTileScreenSpaceErrors, + &priorityQueue = this->_visibleTileQueue, + &octree = this->_octree]( + size_t index, + const Cesium3DTilesSelection::Tile* pTile, + const UCesiumGltfVoxelComponent* pVoxel) { + double sse = VisibleTileScreenSpaceErrors[index]; + FVoxelOctree::Node* pNode = octree.GetNode(pVoxel->tileId); + if (pNode) { + pNode->LastKnownScreenSpaceError = sse; + } + + // Don't create the missing node just yet? It may not be added to the + // tree depending on the priority of other nodes. + priorityQueue.push({pVoxel, sse, computePriority(sse)}); + }); + + if (this->_visibleTileQueue.empty()) { + return; + } + + // Sort the existing nodes in the megatexture by highest to lowest priority. + std::sort( + this->_loadedNodeIds.begin(), + this->_loadedNodeIds.end(), + [&octree = this->_octree]( + const CesiumGeometry::OctreeTileID& lhs, + const CesiumGeometry::OctreeTileID& rhs) { + FVoxelOctree::Node* pLeft = octree.GetNode(lhs); + FVoxelOctree::Node* pRight = octree.GetNode(rhs); + if (!pLeft) { + return false; + } + if (!pRight) { + return true; + } + return computePriority(pLeft->LastKnownScreenSpaceError) > + computePriority(pRight->LastKnownScreenSpaceError); + }); + + bool shouldUpdateOctree = false; + // It is possible for the data textures to not exist (e.g., the default voxel + // material), so check this explicitly. + bool dataTexturesExist = this->_dataTextures.GetTextureCount() > 0; + + size_t existingNodeCount = this->_loadedNodeIds.size(); + size_t destroyedNodeCount = 0; + size_t addedNodeCount = 0; + + if (dataTexturesExist) { + // For all of the visible nodes... + for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { + const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); + const CesiumGeometry::OctreeTileID& currentTileId = + currentTile.pComponent->tileId; + FVoxelOctree::Node* pNode = this->_octree.GetNode(currentTileId); + if (pNode && pNode->DataSlotIndex >= 0) { + // Node has already been loaded into the data textures. + continue; + } + + // Otherwise, check that the data textures have the space to add it. + const UCesiumGltfVoxelComponent* pVoxel = currentTile.pComponent; + size_t addNodeIndex = 0; + if (this->_dataTextures.IsFull()) { + addNodeIndex = existingNodeCount - 1 - destroyedNodeCount; + if (addNodeIndex >= this->_loadedNodeIds.size()) { + // This happens when all of the previously loaded nodes have been + // replaced with new ones. + continue; + } + + destroyedNodeCount++; + + const CesiumGeometry::OctreeTileID& lowestPriorityId = + this->_loadedNodeIds[addNodeIndex]; + FVoxelOctree::Node* pLowestPriorityNode = + this->_octree.GetNode(lowestPriorityId); + + // Release the data slot of the lowest priority node. + this->_dataTextures.Release(pLowestPriorityNode->DataSlotIndex); + pLowestPriorityNode->DataSlotIndex = -1; + + // Attempt to remove the node and simplify the octree. + // Will not succeed if the node's siblings are renderable, or if this + // node contains renderable children. + shouldUpdateOctree |= this->_octree.RemoveNode(lowestPriorityId); + } else { + addNodeIndex = existingNodeCount + addedNodeCount; + addedNodeCount++; + } + + // Create the node if it does not already exist in the tree. + bool createdNewNode = this->_octree.CreateNode(currentTileId); + pNode = this->_octree.GetNode(currentTileId); + pNode->LastKnownScreenSpaceError = currentTile.sse; + + pNode->DataSlotIndex = this->_dataTextures.Add(*pVoxel); + bool addedToDataTexture = (pNode->DataSlotIndex >= 0); + shouldUpdateOctree |= createdNewNode || addedToDataTexture; + + if (!addedToDataTexture) { + continue; + } else if (addNodeIndex < this->_loadedNodeIds.size()) { + this->_loadedNodeIds[addNodeIndex] = currentTileId; + } else { + this->_loadedNodeIds.push_back(currentTileId); + } + } + } else { + // If there are no data textures, then for all of the visible nodes... + for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { + const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); + const CesiumGeometry::OctreeTileID& currentTileId = + currentTile.pComponent->tileId; + // Create the node if it does not already exist in the tree. + shouldUpdateOctree |= this->_octree.CreateNode(currentTileId); + + FVoxelOctree::Node* pNode = this->_octree.GetNode(currentTileId); + pNode->LastKnownScreenSpaceError = currentTile.sse; + // Set to arbitrary index. This will prompt the tile to render even though + // it does not actually have data. + pNode->DataSlotIndex = 0; + } + } + + if (shouldUpdateOctree) { + this->_octree.UpdateTexture(); + } +} + +double FVoxelResources::computePriority(double sse) { + return 10.0 * sse / (sse + 1.0); +} diff --git a/Source/CesiumRuntime/Private/VoxelResources.h b/Source/CesiumRuntime/Private/VoxelResources.h new file mode 100644 index 000000000..f9bd20e24 --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelResources.h @@ -0,0 +1,118 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumCommon.h" +#include "CesiumGltfVoxelComponent.h" +#include "Engine/VolumeTexture.h" +#include "VoxelDataTextures.h" +#include "VoxelOctree.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Cesium3DTilesSelection { +class Tile; +} + +enum class EVoxelGridShape : uint8; +struct FCesiumVoxelClassDescription; + +class FVoxelResources { +public: + /** + * Value constants taken from CesiumJS. + */ + static const uint32 MaximumOctreeTextureWidth = 2048; + static const uint32 MaximumDataTextureMemoryBytes = 512 * 1024 * 1024; + static const uint32 DefaultDataTextureMemoryBytes = 128 * 1024 * 1024; + + /** + * @brief Constructs the resources necessary to render voxel data in an Unreal + * material. + * + * @param pVoxelClass The voxel class description, indicating which metadata + * attributes to encode. + * @param shape The shape of the voxel grid, which affects how voxel data is + * read and stored. + * @param dataDimensions The dimensions of the voxel data in each tile, + * including padding. + * @param featureLevel The RHI feature level associated with the scene. + * @param requestedMemoryPerDataTexture The requested texture memory for the + * data texture constructed for each voxel attribute. + */ + FVoxelResources( + const FCesiumVoxelClassDescription* pVoxelClass, + EVoxelGridShape shape, + const glm::uvec3& dataDimensions, + ERHIFeatureLevel::Type featureLevel, + uint32 requestedMemoryPerDataTexture = DefaultDataTextureMemoryBytes); + ~FVoxelResources(); + + /** + * @brief Retrieves how many tiles there in the megatexture along each + * dimension. + */ + FVector GetTileCount() const; + + /** + * @brief Retrieves the texture containing the encoded octree. + */ + UTexture2D* GetOctreeTexture() const { return this->_octree.GetTexture(); } + + /** + * @brief Retrieves the texture containing the data for the attribute with + * the given ID. Returns nullptr if the attribute does not exist. + */ + UTexture* GetDataTexture(const FString& attributeId) const { + return this->_dataTextures.GetDataTexture(attributeId); + } + + /** + * Updates the resources given the currently visible tiles. + */ + void Update( + const std::vector& VisibleTiles, + const std::vector& VisibleTileScreenSpaceErrors); + +private: + static double computePriority(double sse); + + struct VoxelTileUpdateInfo { + const UCesiumGltfVoxelComponent* pComponent; + double sse; + double priority; + }; + + struct ScreenSpaceErrorGreaterComparator { + bool + operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { + return lhs.priority > rhs.priority; + } + }; + + struct ScreenSpaceErrorLessComparator { + bool + operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { + return lhs.priority < rhs.priority; + } + }; + + FVoxelOctree _octree; + FVoxelDataTextures _dataTextures; + + using MaxPriorityQueue = std::priority_queue< + VoxelTileUpdateInfo, + std::vector, + ScreenSpaceErrorLessComparator>; + + std::vector _loadedNodeIds; + MaxPriorityQueue _visibleTileQueue; +}; diff --git a/Source/CesiumRuntime/Public/Cesium3DTileset.h b/Source/CesiumRuntime/Public/Cesium3DTileset.h index 95c19d593..13bf1d16a 100644 --- a/Source/CesiumRuntime/Public/Cesium3DTileset.h +++ b/Source/CesiumRuntime/Public/Cesium3DTileset.h @@ -8,11 +8,12 @@ #include "Cesium3DTilesetLoadFailureDetails.h" #include "CesiumCreditSystem.h" #include "CesiumEncodedMetadataComponent.h" -#include "CesiumFeaturesMetadataComponent.h" +#include "CesiumFeaturesMetadataDescription.h" #include "CesiumGeoreference.h" #include "CesiumIonServer.h" #include "CesiumPointCloudShading.h" #include "CesiumSampleHeightResult.h" +#include "CesiumVoxelMetadataComponent.h" #include "CoreMinimal.h" #include "CustomDepthParameters.h" #include "Engine/EngineTypes.h" @@ -35,9 +36,14 @@ class UMaterialInterface; class ACesiumCartographicSelection; class ACesiumCameraManager; class UCesiumBoundingVolumePoolComponent; +class UCesiumVoxelRendererComponent; class CesiumViewExtension; struct FCesiumCamera; +namespace Cesium3DTiles { +struct ExtensionContent3dTilesContentVoxels; +} + namespace Cesium3DTilesSelection { class Tileset; class TilesetView; @@ -1266,6 +1272,13 @@ class CESIUMRUNTIME_API ACesium3DTileset : public AActor { UCesiumEllipsoid* OldEllipsoid, UCesiumEllipsoid* NewEllpisoid); + /** + * Initializes the CesiumVoxelRenderer component for rendering voxel data. + */ + void initializeVoxelRenderer( + const Cesium3DTiles::ExtensionContent3dTilesContentVoxels& + VoxelExtension); + /** * Writes the values of all properties of this actor into the * TilesetOptions, to take them into account during the next @@ -1327,11 +1340,18 @@ class CESIUMRUNTIME_API ACesium3DTileset : public AActor { std::optional _featuresMetadataDescription; + std::optional _voxelClassDescription; PRAGMA_DISABLE_DEPRECATION_WARNINGS std::optional _metadataDescription_DEPRECATED; PRAGMA_ENABLE_DEPRECATION_WARNINGS + /** + * The voxel renderer component used to render voxel data. Only used for voxel + * tilesets. + */ + UCesiumVoxelRendererComponent* _pVoxelRendererComponent = nullptr; + // For debug output uint32_t _lastTilesRendered; uint32_t _lastWorkerThreadTileLoadQueueLength; diff --git a/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h index 50550c2a8..5b51ac847 100644 --- a/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h +++ b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h @@ -3,7 +3,6 @@ #pragma once #include "CesiumFeaturesMetadataDescription.h" -#include "CesiumMetadataComponent.h" #include "Templates/UniquePtr.h" #if WITH_EDITOR @@ -89,9 +88,7 @@ class CESIUMRUNTIME_API UCesiumVoxelMetadataComponent : public UActorComponent { */ UPROPERTY(EditAnywhere, Category = "Cesium") UMaterialFunctionMaterialLayer* TargetMaterialLayer = nullptr; -#endif -#if WITH_EDITOR /** * A preview of the generated custom shader. */ From c7a4b0db0501218a86a674e814c362add2e989b6 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 7 Mar 2025 11:06:10 -0500 Subject: [PATCH 04/34] Implement load pipeline, add material files --- Content/Materials/CesiumVoxelTemplate.hlsl | 4 - .../Materials/Instances/MI_CesiumVoxel.uasset | Bin 0 -> 11888 bytes .../Materials/Layers/ML_CesiumVoxel.uasset | Bin 0 -> 111321 bytes .../Private/CesiumGltfComponent.cpp | 297 +++++++++++++++++- .../Private/CesiumGltfVoxelComponent.h | 5 - .../Private/CesiumVoxelRendererComponent.cpp | 2 +- Source/CesiumRuntime/Private/LoadGltfResult.h | 4 +- Source/CesiumRuntime/Private/VoxelOctree.cpp | 4 +- 8 files changed, 290 insertions(+), 26 deletions(-) create mode 100644 Content/Materials/Instances/MI_CesiumVoxel.uasset create mode 100644 Content/Materials/Layers/ML_CesiumVoxel.uasset diff --git a/Content/Materials/CesiumVoxelTemplate.hlsl b/Content/Materials/CesiumVoxelTemplate.hlsl index b4a46eb67..fc1f12eb2 100644 --- a/Content/Materials/CesiumVoxelTemplate.hlsl +++ b/Content/Materials/CesiumVoxelTemplate.hlsl @@ -855,10 +855,6 @@ struct VoxelDataTextures // 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) - { - - } float3 VoxelCoords = floor(LocalUV * float3(GridDimensions)); // Account for padding diff --git a/Content/Materials/Instances/MI_CesiumVoxel.uasset b/Content/Materials/Instances/MI_CesiumVoxel.uasset new file mode 100644 index 0000000000000000000000000000000000000000..d5328c551aa803829762679379ceaa6f0ddb1236 GIT binary patch literal 11888 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 N&CYC--`aq6cIG1a&5gcEDZ4DV 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 Blyd+8 literal 0 HcmV?d00001 diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index ce931f237..9d7514ed6 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp @@ -44,11 +44,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -1820,6 +1822,218 @@ static void loadIndexedPrimitive( } } +namespace { +bool dimensionsAreConsideredEqual( + EVoxelGridShape gridShape, + const std::vector& gltfDimensions, + const std::vector tilesetDimensions) { + switch (gridShape) { + case EVoxelGridShape::Box: + case EVoxelGridShape::Cylinder: + // Because glTF is y-up and 3D Tiles is z-up, it is correct for the y- and + // z-dimensions to be swapped between the tileset.json and its tiles. + return gltfDimensions[0] == tilesetDimensions[0] && + gltfDimensions[1] == tilesetDimensions[2] && + gltfDimensions[2] == tilesetDimensions[1]; + default: + // For ellipsoid voxels, dimensions are not affected by the y-up + // transformation. + return gltfDimensions == tilesetDimensions; + } +} + +bool paddingIsConsideredEqual( + EVoxelGridShape gridShape, + const std::optional& gltfPadding, + const std::optional& tilesetPadding) { + if (gltfPadding.has_value() != tilesetPadding.has_value()) { + return false; + } + + if (!gltfPadding.has_value() && !tilesetPadding.has_value()) { + return true; + } + + return dimensionsAreConsideredEqual( + gridShape, + gltfPadding->before, + tilesetPadding->before) && + dimensionsAreConsideredEqual( + gridShape, + gltfPadding->after, + tilesetPadding->after); +} +} // namespace + +static void loadVoxels( + LoadedPrimitiveResult& primitiveResult, + const CesiumGltf::MeshPrimitive& primitive, + const CesiumGltf::ExtensionExtPrimitiveVoxels& voxelExtension, + const glm::dmat4x4& transform, + const CreatePrimitiveOptions& options) { + TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::loadVoxels) + if (primitive.mode != 2147483647) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Primitive mode %d does not match the voxel constant for EXT_primitive_voxels (2147483647). Skipped."), + primitive.mode); + return; + } + + const CreateVoxelOptions* pVoxelOptions = + options.pMeshOptions->pNodeOptions->pModelOptions->pVoxelOptions; + if (!pVoxelOptions || !pVoxelOptions->pTilesetExtension) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "glTF voxel primitive can't be rendered because the tileset does not contain 3DTILES_content_voxels. Skipped.")); + return; + } + + const Cesium3DTiles::ExtensionContent3dTilesContentVoxels* pTilesetExtension = + pVoxelOptions->pTilesetExtension; + if (!dimensionsAreConsideredEqual( + pVoxelOptions->gridShape, + voxelExtension.dimensions, + pTilesetExtension->dimensions)) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Voxel primitive dimensions do not match the dimensions specified by the tileset. Skipped.")); + return; + } + + if (!paddingIsConsideredEqual( + pVoxelOptions->gridShape, + voxelExtension.padding, + pTilesetExtension->padding)) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Voxel primitive padding does not match the padding specified by the tileset. Skipped.")); + return; + } + + const CesiumGltf::ExtensionMeshPrimitiveExtStructuralMetadata* + pPrimitiveMetadata = primitive.getExtension< + CesiumGltf::ExtensionMeshPrimitiveExtStructuralMetadata>(); + if (!pPrimitiveMetadata || pPrimitiveMetadata->propertyAttributes.empty()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "glTF voxel primitive is missing EXT_structural_metadata. Skipped.")); + return; + } + + const CesiumGltf::ExtensionModelExtStructuralMetadata* pModelMetadata = + options.pMeshOptions->pNodeOptions->pHalfConstructedModelResult + ->pMetadata; + if (!pModelMetadata || pModelMetadata->propertyAttributes.empty()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "glTF voxel primitive is attached to a model without EXT_structural_metadata. Skipped.")); + return; + } + + const CesiumGltf::Model& model = + *options.pMeshOptions->pNodeOptions->pModelOptions->pModel; + const CesiumGltf::Mesh& mesh = model.meshes[options.pMeshOptions->meshIndex]; + + const std::string name = getPrimitiveName(model, mesh, primitive); + primitiveResult.name = name; + + const CesiumGltf::PropertyAttribute* pPropertyAttribute = nullptr; + + // Find the property attribute that shares the same class property as the + // tileset. + for (int32_t index : pPrimitiveMetadata->propertyAttributes) { + if (index < 0 || + index > + static_cast(pPrimitiveMetadata->propertyAttributes.size())) { + continue; + } + + const CesiumGltf::PropertyAttribute* pAttribute = + &pModelMetadata->propertyAttributes[index]; + if (pAttribute->classProperty == pTilesetExtension->classProperty) { + pPropertyAttribute = pAttribute; + break; + } + } + + if (!pPropertyAttribute) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "glTF voxel primitive is missing the required voxel metadata class. Skipped.")); + return; + } + + primitiveResult.voxelResult.emplace(); + + for (auto propertyIt : pPropertyAttribute->properties) { + const std::string& attributeNameInPrimitive = propertyIt.second.attribute; + if (primitive.attributes.find(attributeNameInPrimitive) == + primitive.attributes.end()) { + continue; + } + + int32 index = primitive.attributes.at(propertyIt.second.attribute); + + const CesiumGltf::Accessor* pAccessor = + model.getSafe(&model.accessors, index); + if (!pAccessor || pAccessor->count != pVoxelOptions->voxelCount) { + continue; + } + + const CesiumGltf::BufferView* pBufferView = + model.getSafe( + &model.bufferViews, + pAccessor->bufferView); + if (!pBufferView) { + continue; + } + + if (pAccessor->count * pAccessor->computeBytesPerVertex() != + pBufferView->byteLength) { + // Don't try to copy if the buffer view does not match the expected size. + continue; + } + + const CesiumGltf::Buffer* pBuffer = + model.getSafe(&model.buffers, pBufferView->buffer); + if (!pBuffer) { + continue; + } + + size_t totalBytes = + static_cast(pBufferView->byteOffset + pBufferView->byteLength); + if (totalBytes > pBuffer->cesium.data.size()) { + continue; + } + + primitiveResult.voxelResult->attributeBuffers.Add( + FString(propertyIt.first.c_str()), + ValidatedVoxelBuffer{pBuffer, pBufferView}); + } + + primitiveResult.primitiveIndex = options.primitiveIndex; + primitiveResult.transform = transform * yInvertMatrix; + primitiveResult.Metadata = + pPrimitiveMetadata + ? FCesiumPrimitiveMetadata(primitive, *pPrimitiveMetadata) + : FCesiumPrimitiveMetadata(); +} + static void loadPrimitive( LoadedPrimitiveResult& result, const glm::dmat4x4& transform, @@ -1833,6 +2047,18 @@ static void loadPrimitive( model.meshes[options.pMeshOptions->meshIndex] .primitives[options.primitiveIndex]; + if (primitive.attributes.empty()) { + return; + } + + const CesiumGltf::ExtensionExtPrimitiveVoxels* pVoxelExtension = + primitive.getExtension(); + if (pVoxelExtension) { + // Special path for voxels. + loadVoxels(result, primitive, *pVoxelExtension, transform, options); + return; + } + auto positionAccessorIt = primitive.attributes.find("POSITION"); if (positionAccessorIt == primitive.attributes.end()) { // This primitive doesn't have a POSITION semantic, ignore it. @@ -1896,7 +2122,7 @@ static void loadMesh( loadPrimitive(primitiveResult, transform, primitiveOptions, ellipsoid); // if it doesn't have render data, then it can't be loaded - if (!primitiveResult.RenderData) { + if (!primitiveResult.RenderData && !primitiveResult.voxelResult) { result->primitiveResults.pop_back(); } } @@ -2276,6 +2502,7 @@ static void loadModelMetadata( } }); + result.pMetadata = pModelMetadata; result.Metadata = FCesiumModelMetadata(model, *pModelMetadata); const FCesiumFeaturesMetadataDescription* pFeaturesMetadataDescription = @@ -3296,6 +3523,50 @@ static void loadPrimitiveGameThreadPart( } } +static void loadVoxelsGameThreadPart( + CesiumGltf::Model& model, + UCesiumGltfComponent* pGltf, + LoadedPrimitiveResult& loadResult, + const Cesium3DTilesSelection::Tile& tile, + ACesium3DTileset* pTilesetActor) { + if (loadResult.voxelResult->attributeBuffers.IsEmpty()) { + UE_LOG( + LogCesium, + Warning, + TEXT("Voxel primitive has no valid attributes; skipped.")); + return; + } + + const CesiumGeometry::OctreeTileID* tileId = + std::get_if(&tile.getTileID()); + if (!tileId) { + return; + } + +#if DEBUG_GLTF_ASSET_NAMES + FName componentName = createSafeName(loadResult.name, ""); +#else + FName componentName = ""; +#endif + + CesiumGltf::MeshPrimitive& meshPrimitive = + model.meshes[loadResult.meshIndex].primitives[loadResult.primitiveIndex]; + UCesiumGltfVoxelComponent* pVoxel = + NewObject(pGltf, componentName); + + pVoxel->tileId = *tileId; + pVoxel->attributeBuffers = + std::move(loadResult.voxelResult->attributeBuffers); + + pVoxel->SetMobility(pGltf->Mobility); + pVoxel->SetupAttachment(pGltf); + + { + TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::RegisterComponent) + pVoxel->RegisterComponent(); + } +} + /*static*/ CesiumAsync::Future UCesiumGltfComponent::CreateOffGameThread( const CesiumAsync::AsyncSystem& AsyncSystem, @@ -3364,16 +3635,20 @@ UCesiumGltfComponent::CreateOffGameThread( if (node.meshResult) { for (LoadedPrimitiveResult& primitive : node.meshResult->primitiveResults) { - loadPrimitiveGameThreadPart( - model, - Gltf, - primitive, - cesiumToUnrealTransform, - tile, - createNavCollision, - pTilesetActor, - node.InstanceTransforms, - node.pInstanceFeatures); + if (primitive.voxelResult) { + loadVoxelsGameThreadPart(model, Gltf, primitive, tile, pTilesetActor); + } else { + loadPrimitiveGameThreadPart( + model, + Gltf, + primitive, + cesiumToUnrealTransform, + tile, + createNavCollision, + pTilesetActor, + node.InstanceTransforms, + node.pInstanceFeatures); + } } } } diff --git a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h index cca11bba8..fc81485f6 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h +++ b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h @@ -52,11 +52,6 @@ class UCesiumGltfVoxelComponent : public USceneComponent { void BeginDestroy(); - ACesium3DTileset* pTilesetActor = nullptr; - const CesiumGltf::Model* pModel = nullptr; - const CesiumGltf::MeshPrimitive* pMeshPrimitive = nullptr; - CesiumGeometry::OctreeTileID tileId; - TMap attributeBuffers; }; diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index b702e7e72..ca70b9bf8 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -31,7 +31,7 @@ UCesiumVoxelRendererComponent::UCesiumVoxelRendererComponent() ConstructorHelpers::FObjectFinder CubeMesh; FConstructorStatics() : DefaultMaterial(TEXT( - "/CesiumForUnreal/Materials/Instances/MI_CesiumVoxels.MI_CesiumVoxels")), + "/CesiumForUnreal/Materials/Instances/MI_CesiumVoxel.MI_CesiumVoxel")), CubeMesh(TEXT("/Engine/BasicShapes/Cube.Cube")) {} }; static FConstructorStatics ConstructorStatics; diff --git a/Source/CesiumRuntime/Private/LoadGltfResult.h b/Source/CesiumRuntime/Private/LoadGltfResult.h index 1431b27a7..08add10d5 100644 --- a/Source/CesiumRuntime/Private/LoadGltfResult.h +++ b/Source/CesiumRuntime/Private/LoadGltfResult.h @@ -34,7 +34,7 @@ namespace LoadGltfResult { /** * Represents the result of loading a glTF voxel primitive on a load thread. */ -struct LoadVoxelResult { +struct LoadedVoxelResult { TMap attributeBuffers; }; @@ -163,7 +163,7 @@ struct LoadedPrimitiveResult { #pragma endregion - std::optional voxelResult = std::nullopt; + std::optional voxelResult = std::nullopt; }; /** diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index deb5ddd04..b10a052ae 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -206,9 +206,7 @@ void FVoxelOctree::InitializeTexture(uint32 Width, uint32 MaximumTileCount) { pResource](FRHICommandListImmediate& RHICmdList) { pResource->SetTextureReference( pTexture->TextureReference.TextureReferenceRHI); - pResource->InitResource( - FRHICommandListImmediate::Get()); // Init Resource now requires a - // command list. + pResource->InitResource(FRHICommandListImmediate::Get()); }); } From cb3a0e16b4fa281ac4665c6c29058457e40df6fe Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 7 Mar 2025 15:09:03 -0500 Subject: [PATCH 05/34] Support connection remapping, fix property bug --- .../Private/CesiumVoxelMetadataComponent.cpp | 215 ++++++++++++++---- .../CesiumFeaturesMetadataDescription.h | 4 +- 2 files changed, 171 insertions(+), 48 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index a4d46c6a0..a1366167b 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -52,6 +52,8 @@ using namespace EncodedFeaturesMetadata; using namespace GenerateMaterialUtility; +static const FString RaymarchDescription = "Voxel Raymarch"; + UCesiumVoxelMetadataComponent::UCesiumVoxelMetadataComponent() : UActorComponent() { // Structure to hold one-time initialization @@ -212,6 +214,11 @@ void UCesiumVoxelMetadataComponent::AutoFill() { } namespace { +struct VoxelMetadataClassification : public MaterialNodeClassification { + UMaterialExpressionCustom* RaymarchNode = nullptr; + UMaterialExpressionMaterialFunctionCall* BreakFloat4Node = nullptr; +}; + struct MaterialResourceLibrary { FString HlslShaderTemplate; UMaterialFunctionMaterialLayer* MaterialLayerTemplate; @@ -473,16 +480,36 @@ void UCesiumVoxelMetadataComponent::UpdateShaderPreview() { this->CustomShaderPreview = LazyPrintf.GetResultString(); } -static MaterialNodeClassification +static VoxelMetadataClassification ClassifyNodes(UMaterialFunctionMaterialLayer* Layer) { - MaterialNodeClassification Classification; - for (const TObjectPtr& Node : + VoxelMetadataClassification Classification; + for (const TObjectPtr& pNode : Layer->GetExpressionCollection().Expressions) { // Check if this node is marked as autogenerated. - if (Node->Desc.StartsWith( + if (pNode->Desc.StartsWith( AutogeneratedMessage, ESearchCase::Type::CaseSensitive)) { - Classification.AutoGeneratedNodes.Add(Node); + Classification.AutoGeneratedNodes.Add(pNode); + + UMaterialExpressionCustom* pCustomNode = + Cast(pNode); + if (pCustomNode && + pCustomNode->Description.Contains(RaymarchDescription)) { + Classification.RaymarchNode = pCustomNode; + continue; + } + + UMaterialExpressionMaterialFunctionCall* pFunctionCall = + Cast(pNode); + const FString& FunctionName = + pFunctionCall && pFunctionCall->MaterialFunction + ? pFunctionCall->MaterialFunction->GetName() + : FString(); + if (FunctionName.Contains("BreakOutFloat4")) { + Classification.BreakFloat4Node = pFunctionCall; + } + } else { + Classification.UserAddedNodes.Add(pNode); } } return Classification; @@ -490,8 +517,58 @@ ClassifyNodes(UMaterialFunctionMaterialLayer* Layer) { static void ClearAutoGeneratedNodes( UMaterialFunctionMaterialLayer* Layer, - MaterialGenerationState& MaterialState) { - MaterialNodeClassification Classification = ClassifyNodes(Layer); + TMap>& ConnectionInputRemap, + TMap>& ConnectionOutputRemap) { + VoxelMetadataClassification Classification = ClassifyNodes(Layer); + // Determine which user-added connections to remap when regenerating the + // voxel raymarch node. + UMaterialExpressionCustom* pRaymarchNode = Classification.RaymarchNode; + if (pRaymarchNode && pRaymarchNode->Outputs.Num() > 0) { + FExpressionOutput& Output = pRaymarchNode->Outputs[0]; + FString Key = + pRaymarchNode->GetDescription() + Output.OutputName.ToString(); + + // Look for user-made connections to this property. + TArray Connections; + for (UMaterialExpression* UserNode : Classification.UserAddedNodes) { + for (FExpressionInput* Input : UserNode->GetInputsView()) { + if (Input->Expression == pRaymarchNode && Input->OutputIndex == 0) { + Connections.Add(Input); + Input->Expression = nullptr; + } + } + } + + ConnectionOutputRemap.Emplace(MoveTemp(Key), MoveTemp(Connections)); + } + + // Determine which user-added connections to remap when regenerating the + // break node. This is primarily used to break out the alpha channel, but + // check all outputs just in case the user has made connections. + UMaterialExpressionMaterialFunctionCall* pBreakNode = + Classification.BreakFloat4Node; + if (pBreakNode) { + int32 OutputIndex = 0; + for (const FExpressionOutput& Output : pBreakNode->Outputs) { + FString Key = pBreakNode->GetDescription() + Output.OutputName.ToString(); + + // Look for user-made connections to this property. + TArray Connections; + for (UMaterialExpression* UserNode : Classification.UserAddedNodes) { + for (FExpressionInput* Input : UserNode->GetInputsView()) { + if (Input->Expression == pBreakNode && + Input->OutputIndex == OutputIndex) { + Connections.Add(Input); + Input->Expression = nullptr; + } + } + } + + ConnectionOutputRemap.Emplace(MoveTemp(Key), MoveTemp(Connections)); + ++OutputIndex; + } + } + // Remove auto-generated nodes. for (UMaterialExpression* AutoGeneratedNode : Classification.AutoGeneratedNodes) { @@ -623,7 +700,7 @@ static void GenerateMaterialNodes( NewExpression->UpdateMaterialExpressionGuid(true, true); auto* CustomNode = Cast(NewExpression); - if (CustomNode && CustomNode->GetDescription() == "Voxel Raymarch") { + if (CustomNode && CustomNode->GetDescription() == RaymarchDescription) { RaymarchNode = CustomNode; continue; } @@ -677,6 +754,9 @@ static void GenerateMaterialNodes( } } + // Save this to offset some nodes later. + int32 SetMaterialAttributesOffset = + BreakFloat4Node->MaterialExpressionEditorX; NodeX = DataSectionX; NodeY = DataSectionY; @@ -770,7 +850,7 @@ static void GenerateMaterialNodes( MaterialState.OneTimeGeneratedNodes.Add(InputMaterial); } - NodeX += 10 * Incr; + NodeX += SetMaterialAttributesOffset + Incr; UMaterialExpressionSetMaterialAttributes* SetMaterialAttributes = nullptr; for (const TObjectPtr& ExistingNode : @@ -786,40 +866,41 @@ static void GenerateMaterialNodes( if (!SetMaterialAttributes) { SetMaterialAttributes = NewObject(pTargetLayer); + SetMaterialAttributes->MaterialExpressionEditorX = NodeX; + SetMaterialAttributes->MaterialExpressionEditorY = NodeY; MaterialState.OneTimeGeneratedNodes.Add(SetMaterialAttributes); } - SetMaterialAttributes->Inputs.Reset(3); - SetMaterialAttributes->AttributeSetTypes.Reset(2); - - SetMaterialAttributes->Inputs.EmplaceAt(0, FExpressionInput()); - SetMaterialAttributes->Inputs[0].Expression = InputMaterial; - - SetMaterialAttributes->Inputs.EmplaceAt(1, FExpressionInput()); - SetMaterialAttributes->Inputs[1].Connect(0, RaymarchNode); - SetMaterialAttributes->Inputs[1].InputName = "Base Color"; - - SetMaterialAttributes->Inputs.EmplaceAt(2, FExpressionInput()); - SetMaterialAttributes->Inputs[2].Connect(3, BreakFloat4Node); - SetMaterialAttributes->Inputs[2].InputName = "Opacity"; - - // SetMaterialAttributes needs to manage an internal list of which attributes - // were selected. - const TArray& OrderedVisibleAttributes = - FMaterialAttributeDefinitionMap::GetOrderedVisibleAttributeList(); - for (const FGuid& AttributeID : OrderedVisibleAttributes) { - const FString& name = - FMaterialAttributeDefinitionMap::GetAttributeName(AttributeID); - if (name == "BaseColor") { - SetMaterialAttributes->AttributeSetTypes.EmplaceAt(0, AttributeID); - } else if (name == "Opacity") { - SetMaterialAttributes->AttributeSetTypes.EmplaceAt(1, AttributeID); + if (SetMaterialAttributes->Inputs.Num() <= 1) { + SetMaterialAttributes->Inputs.Reset(3); + SetMaterialAttributes->AttributeSetTypes.Reset(2); + + SetMaterialAttributes->Inputs.EmplaceAt(0, FExpressionInput()); + SetMaterialAttributes->Inputs[0].Expression = InputMaterial; + + SetMaterialAttributes->Inputs.EmplaceAt(1, FExpressionInput()); + SetMaterialAttributes->Inputs[1].Connect(0, RaymarchNode); + SetMaterialAttributes->Inputs[1].InputName = "Base Color"; + + SetMaterialAttributes->Inputs.EmplaceAt(2, FExpressionInput()); + SetMaterialAttributes->Inputs[2].Connect(3, BreakFloat4Node); + SetMaterialAttributes->Inputs[2].InputName = "Opacity"; + + // SetMaterialAttributes needs to manage an internal list of which + // attributes were selected. + const TArray& OrderedVisibleAttributes = + FMaterialAttributeDefinitionMap::GetOrderedVisibleAttributeList(); + for (const FGuid& AttributeID : OrderedVisibleAttributes) { + const FString& name = + FMaterialAttributeDefinitionMap::GetAttributeName(AttributeID); + if (name == "BaseColor") { + SetMaterialAttributes->AttributeSetTypes.EmplaceAt(0, AttributeID); + } else if (name == "Opacity") { + SetMaterialAttributes->AttributeSetTypes.EmplaceAt(1, AttributeID); + } } } - SetMaterialAttributes->MaterialExpressionEditorX = NodeX; - SetMaterialAttributes->MaterialExpressionEditorY = NodeY; - NodeX += 2 * Incr; UMaterialExpressionFunctionOutput* OutputMaterial = nullptr; @@ -835,13 +916,52 @@ static void GenerateMaterialNodes( if (!OutputMaterial) { OutputMaterial = NewObject(pTargetLayer); + OutputMaterial->A = FMaterialAttributesInput(); + OutputMaterial->A.Expression = SetMaterialAttributes; + OutputMaterial->MaterialExpressionEditorX = NodeX; + OutputMaterial->MaterialExpressionEditorY = NodeY; MaterialState.OneTimeGeneratedNodes.Add(OutputMaterial); } +} + +static void RemapUserConnections( + UMaterialFunctionMaterialLayer* Layer, + TMap>& ConnectionInputRemap, + TMap>& ConnectionOutputRemap) { + + VoxelMetadataClassification Classification = ClassifyNodes(Layer); - OutputMaterial->MaterialExpressionEditorX = NodeX; - OutputMaterial->MaterialExpressionEditorY = NodeY; - OutputMaterial->A = FMaterialAttributesInput(); - OutputMaterial->A.Expression = SetMaterialAttributes; + UMaterialExpressionCustom* pRaymarchNode = Classification.RaymarchNode; + if (pRaymarchNode && pRaymarchNode->Outputs.Num() > 0) { + FExpressionOutput& Output = pRaymarchNode->Outputs[0]; + FString Key = + pRaymarchNode->GetDescription() + Output.OutputName.ToString(); + + TArray* pConnections = ConnectionOutputRemap.Find(Key); + if (pConnections) { + for (FExpressionInput* pConnection : *pConnections) { + pConnection->Connect(0, pRaymarchNode); + } + } + } + + if (Classification.BreakFloat4Node) { + int32 OutputIndex = 0; + for (const FExpressionOutput& Output : + Classification.BreakFloat4Node->Outputs) { + FString Key = Classification.BreakFloat4Node->GetDescription() + + Output.OutputName.ToString(); + + TArray* pConnections = ConnectionOutputRemap.Find(Key); + if (pConnections) { + for (FExpressionInput* pConnection : *pConnections) { + pConnection->Connect(OutputIndex, Classification.BreakFloat4Node); + } + } + + ++OutputIndex; + } + } } void UCesiumVoxelMetadataComponent::GenerateMaterial() { @@ -890,14 +1010,17 @@ void UCesiumVoxelMetadataComponent::GenerateMaterial() { MaterialGenerationState MaterialState; - ClearAutoGeneratedNodes(this->TargetMaterialLayer, MaterialState); + ClearAutoGeneratedNodes( + this->TargetMaterialLayer, + MaterialState.ConnectionInputRemap, + MaterialState.ConnectionOutputRemap); GenerateMaterialNodes(this, MaterialState, ResourceLibrary); MoveNodesToMaterialLayer(MaterialState, this->TargetMaterialLayer); - // RemapUserConnections( - // this->TargetMaterialLayer, - // MaterialState.ConnectionInputRemap, - // MaterialState.ConnectionOutputRemap); + RemapUserConnections( + this->TargetMaterialLayer, + MaterialState.ConnectionInputRemap, + MaterialState.ConnectionOutputRemap); this->TargetMaterialLayer->PreviewBlendMode = TEnumAsByte(EBlendMode::BLEND_Translucent); diff --git a/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h b/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h index 91640b730..8c1ae86a3 100644 --- a/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h +++ b/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h @@ -253,9 +253,9 @@ struct CESIUMRUNTIME_API FCesiumPropertyAttributePropertyDescription { /** * Describes how the property will be encoded as data on the GPU, if possible. - * TODO: Expose once coercive encoding is supported. + * TODO: Make this EditAnywhere once coercive encoding is supported. */ - // UPROPERTY(EditAnywhere, Category = "Cesium") + UPROPERTY() FCesiumMetadataEncodingDetails EncodingDetails; }; From 3110cf1f1d7042d513c07ae4ef77fdd9dd824540 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 7 Mar 2025 15:30:37 -0500 Subject: [PATCH 06/34] Add missing line --- Content/Materials/CesiumVoxelTemplate.hlsl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Content/Materials/CesiumVoxelTemplate.hlsl b/Content/Materials/CesiumVoxelTemplate.hlsl index fc1f12eb2..a572302c7 100644 --- a/Content/Materials/CesiumVoxelTemplate.hlsl +++ b/Content/Materials/CesiumVoxelTemplate.hlsl @@ -458,6 +458,9 @@ struct ShapeUtility } RayIntersections result = ListState.GetFirstIntersections(Intersections); + // Set start to 0.0 when ray is inside the shape. + result.Entry.t = max(result.Entry.t, 0.0); + return result; } From 2d49893ee7808e060e644b6f9b08b3f3c5bb88a6 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 12 May 2025 13:37:50 -0400 Subject: [PATCH 07/34] Feedback from review --- Source/CesiumRuntime/Private/CesiumGltfComponent.cpp | 6 +++--- .../CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp | 2 -- Source/CesiumRuntime/Private/VoxelResources.cpp | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index 9d7514ed6..0c1436491 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp @@ -1826,7 +1826,7 @@ namespace { bool dimensionsAreConsideredEqual( EVoxelGridShape gridShape, const std::vector& gltfDimensions, - const std::vector tilesetDimensions) { + const std::vector& tilesetDimensions) { switch (gridShape) { case EVoxelGridShape::Box: case EVoxelGridShape::Cylinder: @@ -1956,7 +1956,7 @@ static void loadVoxels( // tileset. for (int32_t index : pPrimitiveMetadata->propertyAttributes) { if (index < 0 || - index > + index >= static_cast(pPrimitiveMetadata->propertyAttributes.size())) { continue; } @@ -3648,7 +3648,7 @@ UCesiumGltfComponent::CreateOffGameThread( pTilesetActor, node.InstanceTransforms, node.pInstanceFeatures); - } + } } } } diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index a1366167b..ee0b0dd73 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -613,8 +613,6 @@ static void GenerateNodesForMetadataPropertyTransforms( OffsetInput.Input.Expression = Parameter; } - FString swizzle = GetSwizzleForEncodedType(Type); - if (PropertyDetails.bHasNoDataValue) { NodeY += Incr; FString ParameterName = PropertyName + MaterialPropertyNoDataSuffix; diff --git a/Source/CesiumRuntime/Private/VoxelResources.cpp b/Source/CesiumRuntime/Private/VoxelResources.cpp index 76e10af60..9eb57a2cd 100644 --- a/Source/CesiumRuntime/Private/VoxelResources.cpp +++ b/Source/CesiumRuntime/Private/VoxelResources.cpp @@ -37,9 +37,7 @@ FVoxelResources::FVoxelResources( this->_loadedNodeIds.reserve(maximumTileCount); } -FVoxelResources::~FVoxelResources() { - // TODO cleanup? -} +FVoxelResources::~FVoxelResources() {} FVector FVoxelResources::GetTileCount() const { auto tileCount = this->_dataTextures.GetTileCountAlongAxes(); From 86e9a15548f436eb0577381117a0be33bd86b978 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 29 May 2025 16:58:11 -0400 Subject: [PATCH 08/34] Update cesium-native --- extern/cesium-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/cesium-native b/extern/cesium-native index 790bb78b8..5cd5df91c 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 790bb78b860af84a617389cd9a30aa46d129b542 +Subproject commit 5cd5df91c5bd15f7ea226389f03d22013cb79b2d From 4871a1daba752e79bf9090ce6e7c7c30e137fd9d Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 30 May 2025 17:03:44 -0400 Subject: [PATCH 09/34] Update for changes in cesium-native and other bugs --- .../Materials/Layers/ML_CesiumVoxel.uasset | Bin 111321 -> 111226 bytes .../CesiumRuntime/Private/Cesium3DTileset.cpp | 15 ++++++++-- .../Private/CesiumVoxelMetadataComponent.cpp | 27 ++++++++++++------ extern/cesium-native | 2 +- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Content/Materials/Layers/ML_CesiumVoxel.uasset b/Content/Materials/Layers/ML_CesiumVoxel.uasset index cbde08d434cf17c28518d8b368f5754de4ec9e6c..ba8b95845e81a70e2597b9a525fa00f41a0a91f2 100644 GIT binary patch delta 19238 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

{R#My`L=I9?UrY^#lmeD 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 IM?tIq0#pdW_5c6? delta 18823 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 D(%uGv diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 7a7eebe69..34ff3f0b9 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -1172,8 +1172,18 @@ void ACesium3DTileset::LoadTileset() { #endif this->_pTileset->getRootTileAvailableEvent().thenImmediately([thiz = this]() { - const Cesium3DTiles::ExtensionContent3dTilesContentVoxels* pVoxelExtension = - thiz->_pTileset ? thiz->_pTileset->getVoxelContentExtension() : nullptr; + if (!thiz->_pTileset || !thiz->_pTileset->getRootTile()) { + return; + } + + const Cesium3DTilesSelection::TileExternalContent* pExternalContent = + thiz->_pTileset->getRootTile()->getContent().getExternalContent(); + if (!pExternalContent) { + return; + } + + const auto* pVoxelExtension = pExternalContent->getExtension< + Cesium3DTiles::ExtensionContent3dTilesContentVoxels>(); if (pVoxelExtension) { thiz->initializeVoxelRenderer(*pVoxelExtension); } @@ -2387,7 +2397,6 @@ void ACesium3DTileset::initializeVoxelRenderer( this->_pTileset->getRootTile(); if (!pRootTile) { // Not sure how this could happen, but just in case... - CESIUM_ASSERT(false); return; } diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index ee0b0dd73..cd8d16fdc 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -129,10 +129,13 @@ GetValueTypeFromClassProperty(const Cesium3DTiles::ClassProperty& Property) { void AutoFillVoxelClassDescription( FCesiumVoxelClassDescription& Description, - const std::string& VoxelClassID, - const Cesium3DTiles::Class& VoxelClass) { + const Cesium3DTiles::Schema& TilesetSchema, + const std::string& VoxelClassID) { + Description.ID = TilesetSchema.id.c_str(); - for (const auto& propertyIt : VoxelClass.properties) { + const Cesium3DTiles::Class& voxelClass = + TilesetSchema.classes.at(VoxelClassID); + for (const auto& propertyIt : voxelClass.properties) { auto pExistingProperty = Description.Properties.FindByPredicate( [&propertyName = propertyIt.first]( const FCesiumPropertyAttributePropertyDescription& @@ -172,12 +175,19 @@ void UCesiumVoxelMetadataComponent::AutoFill() { const ACesium3DTileset* pOwner = this->GetOwner(); const Cesium3DTilesSelection::Tileset* pTileset = pOwner ? pOwner->GetTileset() : nullptr; - if (!pTileset) { + if (!pTileset || !pTileset->getRootTile()) { + return; + } + + const Cesium3DTilesSelection::TileExternalContent* pExternalContent = + pTileset->getRootTile()->getContent().getExternalContent(); + if (!pExternalContent) { return; } - const Cesium3DTiles::ExtensionContent3dTilesContentVoxels* pVoxelExtension = - pTileset->getVoxelContentExtension(); + const auto* pVoxelExtension = + pExternalContent + ->getExtension(); if (!pVoxelExtension) { UE_LOG( LogCesium, @@ -188,7 +198,6 @@ void UCesiumVoxelMetadataComponent::AutoFill() { return; } - // TODO turn into helper? function const Cesium3DTilesSelection::TilesetMetadata* pMetadata = pTileset->getMetadata(); if (!pMetadata || !pMetadata->schema) { @@ -205,8 +214,8 @@ void UCesiumVoxelMetadataComponent::AutoFill() { AutoFillVoxelClassDescription( this->Description, - voxelClassId, - pMetadata->schema->classes.at(voxelClassId)); + *pMetadata->schema, + voxelClassId); Super::PostEditChange(); diff --git a/extern/cesium-native b/extern/cesium-native index 5cd5df91c..c8625b873 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 5cd5df91c5bd15f7ea226389f03d22013cb79b2d +Subproject commit c8625b87330b56879559be86a0e46452e79219e7 From 5ae3b8f483ae565e6f977055f213202a5035b217 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 2 Jun 2025 17:10:20 -0400 Subject: [PATCH 10/34] Simplify voxel material/shader for review. --- Content/Materials/CesiumVoxelTemplate.hlsl | 214 ++---------------- .../Materials/Instances/MI_CesiumVoxel.uasset | Bin 11888 -> 11778 bytes .../Materials/Layers/ML_CesiumVoxel.uasset | Bin 111226 -> 57129 bytes 3 files changed, 14 insertions(+), 200 deletions(-) diff --git a/Content/Materials/CesiumVoxelTemplate.hlsl b/Content/Materials/CesiumVoxelTemplate.hlsl index a572302c7..f04bec4ba 100644 --- a/Content/Materials/CesiumVoxelTemplate.hlsl +++ b/Content/Materials/CesiumVoxelTemplate.hlsl @@ -161,186 +161,6 @@ struct RayIntersectionUtility } }; -// 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 -#define INTERSECTIONS_LENGTH SHAPE_INTERSECTIONS * 2 - -// HLSL does not like array struct members, so the data has to be stored at the top-level. -// The size is also hardcoded because dynamically sized arrays are not allowed. -float4 miss = float4(0, 0, 0, NO_HIT); -float4 IntersectionList[INTERSECTIONS_LENGTH] = -{ - miss, miss, miss, miss, miss, miss, miss, - miss, miss, miss, miss, miss, miss, miss, -}; - -struct IntersectionListState -{ - // Don't access these member variables directly - call the functions instead. - // Store an array of encoded ray-surface intersections. (See EncodeIntersection). - - int Length; // Set based on ShapeConstant - int Index; // Used to emulate dynamic indexing. - - // The variables below relate to the shapes that the intersection is inside (counting when it is - // on the surface itself). e.g., given a hollow ellipsoid volume, - // count = 1 on the outer ellipsoid, 2 on the inner ellipsoid. - int SurroundingShapeCount; - bool IsInsidePositiveShape; // will be true as long as it is inside any positive shape. - - /** - * Intersections are encoded as float4s: - * - .xyz for the surface normal at the intersection point - * - .w for the T value - * The normal's scale encodes the shape intersection type: - * length(intersection.xyz) = 1: positive shape entry - * length(intersection.xyz) = 2: positive shape exit - * length(intersection.xyz) = 3: negative shape entry - * length(intersection.xyz) = 4: negative shape exit - * - * When the voxel volume is hollow, the "positive" shape is the original volume. - * The "negative" shape is subtracted from the positive shape. - */ - float4 EncodeIntersection(in Intersection Input, bool IsPositive, bool IsEntering) - { - float scale = float(!IsPositive) * 2.0 + float(!IsEntering) + 1.0; - return float4(Input.Normal * scale, Input.t); - } - - /** - * Sort the intersections from min T to max T with bubble sort. Also prepares for iteration - * over the intersections. - * - * Note: If this sorting function changes, some of the intersection tests may need to be updated. - * Search for "Sort()" to find those areas. - */ - void Sort(inout float4 Data[INTERSECTIONS_LENGTH]) - { - const int sortPasses = INTERSECTIONS_LENGTH - 1; - for (int n = sortPasses; n > 0; --n) - { - // Skip to n = Length - 1 - if (n >= Length) - { - continue; - } - - for (int i = 0; i < sortPasses; ++i) - { - // The loop should be: for (i = 0; i < n; ++i) {...} but since loops with - // non-constant conditions are not allowed, this breaks early instead. - if (i >= n) - { - break; - } - - float4 first = Data[i + 0]; - float4 second = Data[i + 1]; - - bool inOrder = first.w <= second.w; - Data[i + 0] = inOrder ? first : second; - Data[i + 1] = inOrder ? second : first; - } - } - - // Prepare initial state for GetNextIntersections() - Index = 0; - SurroundingShapeCount = 0; - IsInsidePositiveShape = false; - } - - RayIntersections GetFirstIntersections(in float4 Data[INTERSECTIONS_LENGTH]) - { - RayIntersections result = (RayIntersections) 0; - result.Entry.t = Data[0].w; - result.Entry.Normal = normalize(Data[0].xyz); - result.Exit.t = Data[1].w; - result.Exit.Normal = normalize(Data[1].xyz); - - return result; - } - - /** - * Gets the intersection at the current value of Index, while managing the state of the ray's - * trajectory with respect to the intersected shapes. - */ - RayIntersections GetNextIntersections(in float4 Data[INTERSECTIONS_LENGTH]) - { - RayIntersections result = (RayIntersections) 0; - result.Entry.t = NO_HIT; - result.Exit.t = NO_HIT; - - if (Index >= Length) - { - return result; - } - - float4 surfaceIntersection = float4(0, 0, 0, NO_HIT); - - for (int i = 0; i < INTERSECTIONS_LENGTH; ++i) - { - // The loop should be: for (i = index; i < loopCount; ++i) {...} but it's not possible - // to loop with non-constant condition. Instead, continue until i = index. - if (i < Index) - { - continue; - } - - Index = i + 1; - - surfaceIntersection = Data[i]; - // Maps from [1-4] -> [0-3] (see EncodeIntersection for the types) - int intersectionType = int(length(surfaceIntersection.xyz) - 0.5); - bool isCurrentShapePositive = intersectionType < 2; - bool isEnteringShape = (intersectionType % 2) == 0; - - SurroundingShapeCount += isEnteringShape ? +1 : -1; - IsInsidePositiveShape = isCurrentShapePositive ? isEnteringShape : IsInsidePositiveShape; - - // True if entering positive shape or exiting negative shape - if (IsInsidePositiveShape && isEnteringShape == isCurrentShapePositive) - { - result.Entry.t = surfaceIntersection.w; - result.Entry.Normal = normalize(surfaceIntersection.xyz); - } - - // True if exiting the outermost positive shape - bool isExitingOutermostShape = !isEnteringShape && isCurrentShapePositive && SurroundingShapeCount == 0; - // True if entering negative shape while being inside a positive one - bool isEnteringNegativeFromPositive = isEnteringShape && !isCurrentShapePositive && SurroundingShapeCount == 2 && IsInsidePositiveShape; - - if (isExitingOutermostShape || isEnteringNegativeFromPositive) - { - result.Exit.t = surfaceIntersection.w; - result.Exit.Normal = normalize(surfaceIntersection.xyz); - // Entry and exit have been found, so the loop can stop - if (isExitingOutermostShape) - { - // After exiting the outermost positive shape, there is nothing left to intersect. Jump to the end. - Index = INTERSECTIONS_LENGTH; - } - break; - } - // Otherwise, keep searching for the correct exit. - } - - return result; - } -}; - -// Use defines instead of real functions to get array access with a non-constant index. - -/** -* Encodes and stores a single intersection. -*/ -#define setSurfaceIntersection(/*inout float4[]*/ list, /*in IntersectionListState*/ state, /*int*/ index, /*Intersection*/ intersection, /*bool*/ isPositive, /*bool*/ isEntering) (list)[(index)] = (state).EncodeIntersection((intersection), (isPositive), (isEntering)) - -/** -* Encodes and stores the given shape intersections, i.e., the intersections where a ray enters and exits a volume. -*/ -#define setShapeIntersections(/*inout float4[]*/ list, /*in IntersectionListState*/ state, /*int*/ pairIndex, /*RayIntersection*/ intersections) (list)[(pairIndex) * 2 + 0] = (state).EncodeIntersection((intersections).Entry, (pairIndex) == 0, true); (list)[(pairIndex) * 2 + 1] = (state).EncodeIntersection((intersections).Exit, (pairIndex) == 0, false) - /*=========================== END RAY + INTERSECTION UTILITY =============================*/ @@ -356,7 +176,6 @@ struct IntersectionListState struct ShapeUtility { RayIntersectionUtility Utils; - IntersectionListState ListState; int ShapeConstant; @@ -384,26 +203,23 @@ struct ShapeUtility /** * Interpret the input bounds (Local Space) according to the voxel grid shape. */ - void Initialize(in int InShapeConstant, in float3 InMinBounds, in float3 InMaxBounds, in float4 PackedData0, in float4 PackedData1, in float4 PackedData2, in float4 PackedData3, in float4 PackedData4, in float4 PackedData5) + void Initialize(in int InShapeConstant) { ShapeConstant = InShapeConstant; - ListState = (IntersectionListState) 0; if (ShapeConstant == BOX) { // Default unit box bounds. - MinBounds = float3(-1, -1, -1); - MaxBounds = float3(1, 1, 1); + MinBounds = -1; + MaxBounds = 1; } } /** * Tests whether the input ray (Unit Space) intersects the box. Outputs the intersections in Unit Space. */ - void IntersectBox(in Ray R, out float4 Intersections[INTERSECTIONS_LENGTH]) + RayIntersections IntersectBox(in Ray R) { - ListState.Length = 2; - // Consider the box as the intersection of the space between 3 pairs of parallel planes. // Compute the distance along the ray to each plane. @@ -422,10 +238,8 @@ struct ShapeUtility if (entryT > exitT) { - result.Entry.t = NO_HIT; - result.Exit.t = NO_HIT; - setShapeIntersections(Intersections, ListState, 0, result); - return; + Intersection miss = Utils.NewMissedIntersection(); + return Utils.NewRayIntersections(miss, miss); } // Compute normals @@ -437,27 +251,27 @@ struct ShapeUtility bool3 isFirstExit = bool3(Utils.Equal(exits, float3(exitT, exitT, exitT))); result.Exit.Normal = float3(isFirstExit) * directions; result.Exit.t = exitT; - - setShapeIntersections(Intersections, ListState, 0, result); + + return result; } /** * Tests whether the input ray (Unit Space) intersects the shape. */ - RayIntersections - IntersectShape(in Ray R, out float4 Intersections[INTERSECTIONS_LENGTH]) + RayIntersections IntersectShape(in Ray R) { + RayIntersections result; + [branch] switch (ShapeConstant) { case BOX: - IntersectBox(R, Intersections); + result = IntersectBox(R); break; default: return Utils.NewRayIntersections(Utils.NewIntersection(NO_HIT, 0), Utils.NewIntersection(NO_HIT, 0)); } - RayIntersections result = ListState.GetFirstIntersections(Intersections); // Set start to 0.0 when ray is inside the shape. result.Entry.t = max(result.Entry.t, 0.0); @@ -885,7 +699,7 @@ struct VoxelDataTextures Octree VoxelOctree; VoxelOctree.ShapeUtils = (ShapeUtility) 0; -VoxelOctree.ShapeUtils.Initialize(ShapeConstant, ShapeMinBounds, ShapeMaxBounds, PackedData0, PackedData1, PackedData2, PackedData3, PackedData4, PackedData5); +VoxelOctree.ShapeUtils.Initialize(ShapeConstant); Ray R = (Ray) 0; R.Origin = RayOrigin; @@ -916,7 +730,7 @@ R.Direction = RayDirection; // Shape UV Space: Voxel space from [0, 1] conforming to the actual voxel volume. The volume could be a box, a part of a cylinder, or a region on an ellipsoid. // (Vanilla) UV Space: Voxel space within an untransformed voxel octree. This can be envisioned as a simple cube spanning [0, 1] in three dimensions. -RayIntersections Intersections = VoxelOctree.ShapeUtils.IntersectShape(R, IntersectionList); +RayIntersections Intersections = VoxelOctree.ShapeUtils.IntersectShape(R); if (Intersections.Entry.t == NO_HIT) { return 0; } diff --git a/Content/Materials/Instances/MI_CesiumVoxel.uasset b/Content/Materials/Instances/MI_CesiumVoxel.uasset index d5328c551aa803829762679379ceaa6f0ddb1236..b6ca70936bf9fb27b8c9c2b041f8f0c1f776ccaf 100644 GIT binary patch delta 4313 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 JnX~Z={tKnpy(s_y diff --git a/Content/Materials/Layers/ML_CesiumVoxel.uasset b/Content/Materials/Layers/ML_CesiumVoxel.uasset index ba8b95845e81a70e2597b9a525fa00f41a0a91f2..20021c251aac5f1b599a7b6f56d65d0170a1afb5 100644 GIT binary patch delta 19259 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 literal 111226 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 GD*u01C1RZb From 8cb81856a230ede1a7b3fc0a10992841631dbb13 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 3 Jun 2025 14:44:53 -0400 Subject: [PATCH 11/34] Pass on cleanup, correctness, etc. --- Content/Materials/CesiumVoxelTemplate.hlsl | 3 +- .../CesiumRuntime/Private/Cesium3DTileset.cpp | 19 +++++------ .../Private/CesiumVoxelRendererComponent.cpp | 23 +++++++------ .../Private/VoxelDataTextures.cpp | 32 ++++++++++++++++--- .../CesiumRuntime/Private/VoxelDataTextures.h | 14 ++++---- Source/CesiumRuntime/Private/VoxelOctree.cpp | 8 ++++- 6 files changed, 67 insertions(+), 32 deletions(-) diff --git a/Content/Materials/CesiumVoxelTemplate.hlsl b/Content/Materials/CesiumVoxelTemplate.hlsl index f04bec4ba..63acd8680 100644 --- a/Content/Materials/CesiumVoxelTemplate.hlsl +++ b/Content/Materials/CesiumVoxelTemplate.hlsl @@ -269,7 +269,7 @@ struct ShapeUtility result = IntersectBox(R); break; default: - return Utils.NewRayIntersections(Utils.NewIntersection(NO_HIT, 0), Utils.NewIntersection(NO_HIT, 0)); + return Utils.NewRayIntersections(Utils.NewMissedIntersection(), Utils.NewMissedIntersection()); } // Set start to 0.0 when ray is inside the shape. @@ -748,6 +748,7 @@ VoxelDataTextures DataTextures; DataTextures.ShapeConstant = ShapeConstant; DataTextures.TileCount = TileCount; +// Account for y-up -> z-up conventions for certain shapes. switch (ShapeConstant) { case BOX: DataTextures.GridDimensions = round(GridDimensions.xzy); diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 34ff3f0b9..653174fcb 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -2136,6 +2136,7 @@ void ACesium3DTileset::Tick(float DeltaTime) { pResult->tilesToRenderThisFrame, pResult->tileScreenSpaceErrorThisFrame); } else { + removeCollisionForTiles(pResult->tilesFadingOut); hideTiles(this->_tilesToHideNextFrame); _tilesToHideNextFrame.clear(); @@ -2377,8 +2378,12 @@ void ACesium3DTileset::RuntimeSettingsChanged( void ACesium3DTileset::initializeVoxelRenderer( const Cesium3DTiles::ExtensionContent3dTilesContentVoxels& VoxelExtension) { - const FCesiumVoxelClassDescription* pVoxelClassDescription = - this->_voxelClassDescription ? &(*this->_voxelClassDescription) : nullptr; + const Cesium3DTilesSelection::Tile* pRootTile = + this->_pTileset->getRootTile(); + if (!pRootTile) { + // Not sure how this would happen, but just in case... + return; + } // Validate that voxel metadata is present. const Cesium3DTilesSelection::TilesetMetadata* pMetadata = @@ -2388,17 +2393,13 @@ void ACesium3DTileset::initializeVoxelRenderer( LogCesium, Error, TEXT( - "Tileset %s contains voxels, but is missing a metadata schema to describe its contents."), + "Tileset %s contains voxels but is missing a metadata schema to describe its contents."), *this->GetName()) return; } - const Cesium3DTilesSelection::Tile* pRootTile = - this->_pTileset->getRootTile(); - if (!pRootTile) { - // Not sure how this could happen, but just in case... - return; - } + const FCesiumVoxelClassDescription* pVoxelClassDescription = + this->_voxelClassDescription ? &(*this->_voxelClassDescription) : nullptr; this->_pVoxelRendererComponent = UCesiumVoxelRendererComponent::Create( this, diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 19aa5c226..7fe7e2d1c 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -67,9 +67,7 @@ void UCesiumVoxelRendererComponent::BeginDestroy() { namespace { EVoxelGridShape getVoxelGridShape( const Cesium3DTilesSelection::BoundingVolume& boundingVolume) { - const CesiumGeometry::OrientedBoundingBox* pBox = - std::get_if(&boundingVolume); - if (pBox) { + if (std::get_if(&boundingVolume)) { return EVoxelGridShape::Box; } @@ -81,16 +79,20 @@ void setVoxelBoxProperties( UMaterialInstanceDynamic* pVoxelMaterial, const CesiumGeometry::OrientedBoundingBox& box) { glm::dmat3 halfAxes = box.getHalfAxes(); + + // The engine-provided Cube extends from [-50, 50], so a scale of 1/50 is + // incorporated into the component's transform to compensate. 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 transform and scale of the box 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. pVoxelMaterial->SetVectorParameterValueByInfo( FMaterialParameterInfo( UTF8_TO_TCHAR("Shape TransformToUnit Row 0"), @@ -344,7 +346,7 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( LogCesium, Error, TEXT( - "Tileset %s contains voxels, but cannot find the metadata class that describes its contents."), + "Tileset %s does not contain the metadata class that is referenced by its voxel content."), *pTilesetActor->GetName()) return nullptr; } @@ -356,7 +358,7 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( UE_LOG( LogCesium, Error, - TEXT("Tileset %s contains voxels but has invalid dimensions."), + TEXT("Tileset %s has invalid voxel grid dimensions."), *pTilesetActor->GetName()) return nullptr; } @@ -445,8 +447,9 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( glm::uvec3 dataDimensions = glm::uvec3(dimensions[0], dimensions[1], dimensions[2]) + paddingBefore + paddingAfter; + if (shape == EVoxelGridShape::Box || shape == EVoxelGridShape::Cylinder) { - // Account for y-up in glTF -> z-up in 3D Tiles. + // Account for the transformation between y-up (glTF) to z-up (3D Tiles). dataDimensions = glm::uvec3(dataDimensions.x, dataDimensions.z, dataDimensions.y); } diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp index 0cf6dde46..9d6b571aa 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp @@ -3,6 +3,7 @@ #include "VoxelDataTextures.h" #include "CesiumGltfVoxelComponent.h" +#include "CesiumLifetime.h" #include "CesiumMetadataPropertyDetails.h" #include "CesiumRuntime.h" #include "CesiumTextureResource.h" @@ -21,7 +22,7 @@ using namespace EncodedFeaturesMetadata; /** * A Cesium texture resource that creates an initially empty `FRHITexture` for - * FVoxelOctree. + * FVoxelDataTextures. */ class FCesiumVoxelDataTextureResource : public FCesiumTextureResource { public: @@ -104,7 +105,9 @@ FVoxelDataTextures::FVoxelDataTextures( _maximumTileCount(0), _propertyMap() { if (!RHISupportsVolumeTextures(featureLevel)) { - // TODO: 2D fallback? + // TODO: 2D fallback? Not sure if this check is the same as + // SupportsVolumeTextureRendering, which is false on Vulkan Android, Metal, + // and OpenGL UE_LOG( LogCesium, Error, @@ -230,12 +233,26 @@ FVoxelDataTextures::FVoxelDataTextures( } } +FVoxelDataTextures ::~FVoxelDataTextures() { + for (auto propertyIt : this->_propertyMap) { + UTexture* pTexture = propertyIt.Value.pTexture; + propertyIt.Value.pTexture = nullptr; + propertyIt.Value.pResource = nullptr; + + if (IsValid(pTexture)) { + pTexture->RemoveFromRoot(); + CesiumLifetime::destroy(pTexture); + } + } +} + /** * NOTE: This function assumes that the data being read from pData is the same - * type that the texture expects. Coercive encoding behavior (similar to what is - * done for CesiumPropertyTableProperty) could be added in the future. + * type that the texture expects. Coercive encoding behavior (similar to what + * is done for CesiumPropertyTableProperty) could be added in the future. */ static void writeTo3DTexture( + UTexture* pTexture, FCesiumTextureResource* pResource, const std::byte* pData, FUpdateTextureRegion3D updateRegion, @@ -244,8 +261,12 @@ static void writeTo3DTexture( return; ENQUEUE_RENDER_COMMAND(Cesium_CopyVoxels) - ([pResource, pData, updateRegion, texelSizeBytes]( + ([pTexture, pResource, pData, updateRegion, texelSizeBytes]( FRHICommandListImmediate& RHICmdList) { + if (!IsValid(pTexture)) { + return; + } + // Pitch = size in bytes of each row of the source image. uint32 srcRowPitch = updateRegion.Width * texelSizeBytes; uint32 srcDepthPitch = @@ -299,6 +320,7 @@ int64 FVoxelDataTextures::Add(const UCesiumGltfVoxelComponent& voxelComponent) { pData += pValidBuffer->pBufferView->byteOffset; writeTo3DTexture( + PropertyIt.Value.pTexture, PropertyIt.Value.pResource, pData, updateRegion, diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.h b/Source/CesiumRuntime/Private/VoxelDataTextures.h index 5c6b8058b..f3e320001 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.h +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.h @@ -48,6 +48,8 @@ class FVoxelDataTextures { ERHIFeatureLevel::Type featureLevel, uint32 requestedMemoryPerTexture); + ~FVoxelDataTextures(); + /** * Gets the maximum number of tiles that can be added to the data * textures. Equivalent to the maximum number of data slots. @@ -66,7 +68,7 @@ class FVoxelDataTextures { * the given ID. Returns nullptr if the attribute does not exist. */ UTexture* GetDataTexture(const FString& attributeId) const { - const Property* pProperty = this->_propertyMap.Find(attributeId); + const PropertyData* pProperty = this->_propertyMap.Find(attributeId); if (pProperty) { return pProperty->pTexture; } @@ -116,15 +118,15 @@ class FVoxelDataTextures { Slot* Previous = nullptr; }; - struct Property { + struct PropertyData { /** * The texture format used to store encoded property values. */ EncodedFeaturesMetadata::EncodedPixelFormat encodedFormat; - // TODO: have to check RHISupportsVolumeTextures(GetFeatureLevel()) - // not sure if same as SupportsVolumeTextureRendering, which is false on - // Vulkan Android, Metal, and OpenGL + /** + * The data texture for this property. + */ UTexture* pTexture; /** @@ -142,5 +144,5 @@ class FVoxelDataTextures { glm::uvec3 _tileCountAlongAxes; uint32 _maximumTileCount; - TMap _propertyMap; + TMap _propertyMap; }; diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index b10a052ae..9bb841506 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -1,6 +1,7 @@ // Copyright 2020-2024 CesiumGS, Inc. and Contributors #include "VoxelOctree.h" +#include "CesiumLifetime.h" #include "CesiumRuntime.h" #include @@ -185,8 +186,13 @@ FVoxelOctree::~FVoxelOctree() { std::vector empty; std::swap(this->_octreeData, empty); - // Can we count on this being freed since it's a UTexture? + UTexture2D* pTexture = this->_pTexture; this->_pTexture = nullptr; + + if (IsValid(pTexture)) { + pTexture->RemoveFromRoot(); + CesiumLifetime::destroy(pTexture); + } } void FVoxelOctree::InitializeTexture(uint32 Width, uint32 MaximumTileCount) { From f7e80a30cbc3a3a9a2833ee529d754c81e085b0b Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 11 Jun 2025 14:08:48 -0400 Subject: [PATCH 12/34] Refactor to use PropertyAttributeProperty --- .../CesiumRuntime/Private/Cesium3DTileset.cpp | 3 +- .../Private/CesiumGltfComponent.cpp | 180 ++++++------------ .../Private/CesiumGltfVoxelComponent.cpp | 2 - .../Private/CesiumGltfVoxelComponent.h | 38 +--- Source/CesiumRuntime/Private/LoadGltfResult.h | 21 +- .../Private/VoxelDataTextures.cpp | 159 +++++++++++----- .../CesiumRuntime/Private/VoxelDataTextures.h | 31 ++- Source/CesiumRuntime/Private/VoxelOctree.cpp | 82 ++++---- Source/CesiumRuntime/Private/VoxelOctree.h | 62 +++--- .../CesiumRuntime/Private/VoxelResources.cpp | 60 +++--- Source/CesiumRuntime/Private/VoxelResources.h | 4 +- .../CesiumFeaturesMetadataDescription.h | 5 +- 12 files changed, 306 insertions(+), 341 deletions(-) diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 653174fcb..af168e3bd 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -1288,8 +1288,7 @@ void ACesium3DTileset::DestroyTileset() { } // Tiles are about to be deleted, so we should not keep raw pointers on them. - // It did crash in Tick() when we trigger refresh events at a high frequency, - // typically if the user clicks a button "frantically"...) + // This would crash in Tick() when if refresh events were triggered frequently. this->_tilesToHideNextFrame.clear(); if (!this->_pTileset) { diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index 175f6b399..00488e364 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp @@ -92,6 +92,8 @@ class HalfConstructedReal : public UCesiumGltfComponent::HalfConstructed { }; } // namespace +const int32_t VoxelPrimitiveMode = 2147483647; + template struct IsAccessorView; template struct IsAccessorView : std::false_type {}; @@ -1936,7 +1938,7 @@ static void loadVoxels( const glm::dmat4x4& transform, const CreatePrimitiveOptions& options) { TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::loadVoxels) - if (primitive.mode != 2147483647) { + if (primitive.mode != VoxelPrimitiveMode) { UE_LOG( LogCesium, Warning, @@ -1995,107 +1997,33 @@ static void loadVoxels( return; } - const CesiumGltf::ExtensionModelExtStructuralMetadata* pModelMetadata = - options.pMeshOptions->pNodeOptions->pHalfConstructedModelResult - ->pMetadata; - if (!pModelMetadata || pModelMetadata->propertyAttributes.empty()) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "glTF voxel primitive is attached to a model without EXT_structural_metadata. Skipped.")); - return; - } - const CesiumGltf::Model& model = *options.pMeshOptions->pNodeOptions->pModelOptions->pModel; const CesiumGltf::Mesh& mesh = model.meshes[options.pMeshOptions->meshIndex]; const std::string name = getPrimitiveName(model, mesh, primitive); primitiveResult.name = name; + primitiveResult.primitiveIndex = options.primitiveIndex; + primitiveResult.transform = transform * yInvertMatrix; + primitiveResult.Metadata = + pPrimitiveMetadata + ? FCesiumPrimitiveMetadata(model, primitive, *pPrimitiveMetadata) + : FCesiumPrimitiveMetadata(); - const CesiumGltf::PropertyAttribute* pPropertyAttribute = nullptr; + TArray propertyAttributes = + UCesiumPrimitiveMetadataBlueprintLibrary::GetPropertyAttributes( + primitiveResult.Metadata); - // Find the property attribute that shares the same class property as the - // tileset. - for (int32_t index : pPrimitiveMetadata->propertyAttributes) { - if (index < 0 || - index >= - static_cast(pPrimitiveMetadata->propertyAttributes.size())) { - continue; - } + FString classAsFString(pTilesetExtension->classProperty.c_str()); - const CesiumGltf::PropertyAttribute* pAttribute = - &pModelMetadata->propertyAttributes[index]; - if (pAttribute->classProperty == pTilesetExtension->classProperty) { - pPropertyAttribute = pAttribute; + for (int32 i = 0; i < propertyAttributes.Num(); i++) { + // Find the property attribute that shares the same class property as the + // tileset. + if (propertyAttributes[i].getClassName() == classAsFString) { + primitiveResult.voxelPropertyAttributeIndex = i; break; } } - - if (!pPropertyAttribute) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "glTF voxel primitive is missing the required voxel metadata class. Skipped.")); - return; - } - - primitiveResult.voxelResult.emplace(); - - for (auto propertyIt : pPropertyAttribute->properties) { - const std::string& attributeNameInPrimitive = propertyIt.second.attribute; - if (primitive.attributes.find(attributeNameInPrimitive) == - primitive.attributes.end()) { - continue; - } - - int32 index = primitive.attributes.at(propertyIt.second.attribute); - - const CesiumGltf::Accessor* pAccessor = - model.getSafe(&model.accessors, index); - if (!pAccessor || pAccessor->count != pVoxelOptions->voxelCount) { - continue; - } - - const CesiumGltf::BufferView* pBufferView = - model.getSafe( - &model.bufferViews, - pAccessor->bufferView); - if (!pBufferView) { - continue; - } - - if (pAccessor->count * pAccessor->computeBytesPerVertex() != - pBufferView->byteLength) { - // Don't try to copy if the buffer view does not match the expected size. - continue; - } - - const CesiumGltf::Buffer* pBuffer = - model.getSafe(&model.buffers, pBufferView->buffer); - if (!pBuffer) { - continue; - } - - size_t totalBytes = - static_cast(pBufferView->byteOffset + pBufferView->byteLength); - if (totalBytes > pBuffer->cesium.data.size()) { - continue; - } - - primitiveResult.voxelResult->attributeBuffers.Add( - FString(propertyIt.first.c_str()), - ValidatedVoxelBuffer{pBuffer, pBufferView}); - } - - primitiveResult.primitiveIndex = options.primitiveIndex; - primitiveResult.transform = transform * yInvertMatrix; - primitiveResult.Metadata = - pPrimitiveMetadata - ? FCesiumPrimitiveMetadata(primitive, *pPrimitiveMetadata) - : FCesiumPrimitiveMetadata(); } static void loadPrimitive( @@ -2185,7 +2113,8 @@ static void loadMesh( loadPrimitive(primitiveResult, transform, primitiveOptions, ellipsoid); // if it doesn't have render data, then it can't be loaded - if (!primitiveResult.RenderData && !primitiveResult.voxelResult) { + if (!primitiveResult.RenderData && + primitiveResult.voxelPropertyAttributeIndex < 0) { result->primitiveResults.pop_back(); } } @@ -2565,7 +2494,6 @@ static void loadModelMetadata( } }); - result.pMetadata = pModelMetadata; result.Metadata = FCesiumModelMetadata(model, *pModelMetadata); const FCesiumFeaturesMetadataDescription* pFeaturesMetadataDescription = @@ -3264,12 +3192,12 @@ static void loadPrimitiveGameThreadPart( FName componentName = ""; #endif - const Cesium3DTilesSelection::BoundingVolume& boundingVolume = - tile.getContentBoundingVolume().value_or(tile.getBoundingVolume()); - CesiumGltf::MeshPrimitive& meshPrimitive = model.meshes[loadResult.meshIndex].primitives[loadResult.primitiveIndex]; + const Cesium3DTilesSelection::BoundingVolume& boundingVolume = + tile.getContentBoundingVolume().value_or(tile.getBoundingVolume()); + UStaticMeshComponent* pMesh = nullptr; ICesiumPrimitive* pCesiumPrimitive = nullptr; if (meshPrimitive.mode == CesiumGltf::MeshPrimitive::Mode::POINTS) { @@ -3659,11 +3587,15 @@ static void loadVoxelsGameThreadPart( LoadedPrimitiveResult& loadResult, const Cesium3DTilesSelection::Tile& tile, ACesium3DTileset* pTilesetActor) { - if (loadResult.voxelResult->attributeBuffers.IsEmpty()) { + TArray attributes = + UCesiumPrimitiveMetadataBlueprintLibrary::GetPropertyAttributes( + loadResult.Metadata); + + if (attributes.IsEmpty()) { UE_LOG( LogCesium, Warning, - TEXT("Voxel primitive has no valid attributes; skipped.")); + TEXT("Voxel primitive has no valid property attributes; skipped.")); return; } @@ -3673,20 +3605,20 @@ static void loadVoxelsGameThreadPart( return; } + int32_t index = *loadResult.voxelPropertyAttributeIndex; + CESIUM_ASSERT(index >= 0 && index < attributes.Num()); + #if DEBUG_GLTF_ASSET_NAMES FName componentName = createSafeName(loadResult.name, ""); #else FName componentName = ""; #endif - CesiumGltf::MeshPrimitive& meshPrimitive = - model.meshes[loadResult.meshIndex].primitives[loadResult.primitiveIndex]; UCesiumGltfVoxelComponent* pVoxel = NewObject(pGltf, componentName); - pVoxel->tileId = *tileId; - pVoxel->attributeBuffers = - std::move(loadResult.voxelResult->attributeBuffers); + pVoxel->TileId = *tileId; + pVoxel->PropertyAttribute = std::move(attributes[index]); pVoxel->SetMobility(pGltf->Mobility); pVoxel->SetupAttachment(pGltf); @@ -3732,45 +3664,51 @@ UCesiumGltfComponent::CreateOffGameThread( // return nullptr; // } - UCesiumGltfComponent* Gltf = NewObject(pTilesetActor); - Gltf->SetMobility(pTilesetActor->GetRootComponent()->Mobility); - Gltf->SetFlags(RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + UCesiumGltfComponent* pGltf = NewObject(pTilesetActor); + pGltf->SetMobility(pTilesetActor->GetRootComponent()->Mobility); + pGltf->SetFlags( + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); - Gltf->Metadata = std::move(pReal->loadModelResult.Metadata); - Gltf->EncodedMetadata = std::move(pReal->loadModelResult.EncodedMetadata); - Gltf->EncodedMetadata_DEPRECATED = + pGltf->Metadata = std::move(pReal->loadModelResult.Metadata); + pGltf->EncodedMetadata = std::move(pReal->loadModelResult.EncodedMetadata); + pGltf->EncodedMetadata_DEPRECATED = std::move(pReal->loadModelResult.EncodedMetadata_DEPRECATED); if (pBaseMaterial) { - Gltf->BaseMaterial = pBaseMaterial; + pGltf->BaseMaterial = pBaseMaterial; } if (pBaseTranslucentMaterial) { - Gltf->BaseMaterialWithTranslucency = pBaseTranslucentMaterial; + pGltf->BaseMaterialWithTranslucency = pBaseTranslucentMaterial; } if (pBaseWaterMaterial) { - Gltf->BaseMaterialWithWater = pBaseWaterMaterial; + pGltf->BaseMaterialWithWater = pBaseWaterMaterial; } - Gltf->CustomDepthParameters = CustomDepthParameters; + pGltf->CustomDepthParameters = CustomDepthParameters; - encodeModelMetadataGameThreadPart(Gltf->EncodedMetadata); + encodeModelMetadataGameThreadPart(pGltf->EncodedMetadata); - if (Gltf->EncodedMetadata_DEPRECATED) { - encodeMetadataGameThreadPart(*Gltf->EncodedMetadata_DEPRECATED); + if (pGltf->EncodedMetadata_DEPRECATED) { + encodeMetadataGameThreadPart(*pGltf->EncodedMetadata_DEPRECATED); } for (LoadedNodeResult& node : pReal->loadModelResult.nodeResults) { if (node.meshResult) { for (LoadedPrimitiveResult& primitive : node.meshResult->primitiveResults) { - if (primitive.voxelResult) { - loadVoxelsGameThreadPart(model, Gltf, primitive, tile, pTilesetActor); + if (primitive.voxelPropertyAttributeIndex) { + loadVoxelsGameThreadPart( + model, + pGltf, + primitive, + tile, + pTilesetActor); } else { loadPrimitiveGameThreadPart( model, - Gltf, + pGltf, primitive, cesiumToUnrealTransform, tile, @@ -3783,9 +3721,9 @@ UCesiumGltfComponent::CreateOffGameThread( } } - Gltf->SetVisibility(false, true); - Gltf->SetCollisionEnabled(ECollisionEnabled::NoCollision); - return Gltf; + pGltf->SetVisibility(false, true); + pGltf->SetCollisionEnabled(ECollisionEnabled::NoCollision); + return pGltf; } UCesiumGltfComponent::UCesiumGltfComponent() : USceneComponent() { diff --git a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp index 53feaa35a..b9816367f 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp @@ -10,7 +10,5 @@ UCesiumGltfVoxelComponent::UCesiumGltfVoxelComponent() { UCesiumGltfVoxelComponent::~UCesiumGltfVoxelComponent() {} void UCesiumGltfVoxelComponent::BeginDestroy() { - this->attributeBuffers.Empty(); - Super::BeginDestroy(); } diff --git a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h index fc81485f6..052990c97 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h +++ b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h @@ -2,44 +2,20 @@ #pragma once +#include "CesiumPrimitiveMetadata.h" #include "CoreMinimal.h" + #include #include #include "CesiumGltfVoxelComponent.generated.h" -namespace CesiumGltf { -struct Model; -struct MeshPrimitive; -struct PropertyAttribute; -struct Accessor; -struct BufferView; -struct Buffer; -} // namespace CesiumGltf - - -class ACesium3DTileset; - -/** - * Pointers to a buffer that has already been validated, such that: - * - The accessor count is equal to the number of total voxels in the grid. - * - The buffer view on the buffer is valid. - * - * This should be replaced when PropertyAttributeProperty is supported, since it - * is functionally the same (and the latter would be more robust). - */ -struct ValidatedVoxelBuffer { - const CesiumGltf::Buffer* pBuffer; - const CesiumGltf::BufferView* pBufferView; -}; - /** * A barebones component representing a glTF voxel primitive. * - * The voxel rendering for an entire tileset is done singlehandedly by - * UCesiumVoxelRendererComponent. Therefore, this component does not hold any - * mesh data itself. Instead, it stores pointers to the glTF primitive for easy - * retrieval of the voxel attributes. + * This does not hold any mesh data itself; instead, it contains the property + * attribute used.UCesiumVoxelRendererComponent takes care of voxel rendering + * for an entire tileset. */ UCLASS() class UCesiumGltfVoxelComponent : public USceneComponent { @@ -52,6 +28,6 @@ class UCesiumGltfVoxelComponent : public USceneComponent { void BeginDestroy(); - CesiumGeometry::OctreeTileID tileId; - TMap attributeBuffers; + CesiumGeometry::OctreeTileID TileId; + FCesiumPropertyAttribute PropertyAttribute; }; diff --git a/Source/CesiumRuntime/Private/LoadGltfResult.h b/Source/CesiumRuntime/Private/LoadGltfResult.h index 1e4e1d133..eeb55b90e 100644 --- a/Source/CesiumRuntime/Private/LoadGltfResult.h +++ b/Source/CesiumRuntime/Private/LoadGltfResult.h @@ -31,13 +31,6 @@ #include namespace LoadGltfResult { -/** - * Represents the result of loading a glTF voxel primitive on a load thread. - */ -struct LoadedVoxelResult { - TMap attributeBuffers; -}; - /** * Represents the result of loading a glTF primitive on a load thread. * Temporarily holds render data that will be used in the Unreal material, as @@ -67,6 +60,7 @@ struct LoadedPrimitiveResult { int32_t materialIndex = -1; glm::dmat4x4 transform{1.0}; + #if ENGINE_VERSION_5_4_OR_HIGHER Chaos::FTriangleMeshImplicitObjectPtr pCollisionMesh = nullptr; #else @@ -170,7 +164,10 @@ struct LoadedPrimitiveResult { #pragma endregion - std::optional voxelResult = std::nullopt; + /** + * The index of the property attribute that is used by voxels. + */ + std::optional voxelPropertyAttributeIndex; }; /** @@ -225,13 +222,5 @@ struct LoadedModelResult { /** For backwards compatibility with CesiumEncodedMetadataComponent. */ std::optional EncodedMetadata_DEPRECATED{}; - - /** - * Points to the actual EXT_structural_metadata extension. Used for voxels - * because property attributes are not yet supported. - * - * TODO: Expand FCesiumModelMetadata so this is not necessary. - */ - const CesiumGltf::ExtensionModelExtStructuralMetadata* pMetadata = nullptr; }; } // namespace LoadGltfResult diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp index 9d6b571aa..159faa5b5 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp @@ -104,18 +104,6 @@ FVoxelDataTextures::FVoxelDataTextures( _tileCountAlongAxes(0), _maximumTileCount(0), _propertyMap() { - if (!RHISupportsVolumeTextures(featureLevel)) { - // TODO: 2D fallback? Not sure if this check is the same as - // SupportsVolumeTextureRendering, which is false on Vulkan Android, Metal, - // and OpenGL - UE_LOG( - LogCesium, - Error, - TEXT( - "Volume textures are not supported. Unable to create the textures necessary for rendering voxels.")) - return; - } - if (!pVoxelClass) { UE_LOG( LogCesium, @@ -129,6 +117,18 @@ FVoxelDataTextures::FVoxelDataTextures( return; } + if (!RHISupportsVolumeTextures(featureLevel)) { + // TODO: 2D fallback? Not sure if this check is the same as + // SupportsVolumeTextureRendering, which is false on Vulkan Android, Metal, + // and OpenGL + UE_LOG( + LogCesium, + Error, + TEXT( + "Volume textures are not supported. Unable to create the textures necessary for rendering voxels.")) + return; + } + // Attributes can take up varying texel sizes based on their type. // So first, identify which attribute is the largest in size. uint32 maximumTexelSizeBytes = 0; @@ -244,6 +244,13 @@ FVoxelDataTextures ::~FVoxelDataTextures() { CesiumLifetime::destroy(pTexture); } } + + this->_propertyMap.Empty(); +} + +UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { + const TextureData* pProperty = this->_propertyMap.Find(attributeId); + return pProperty ? pProperty->pTexture : nullptr; } /** @@ -251,39 +258,94 @@ FVoxelDataTextures ::~FVoxelDataTextures() { * type that the texture expects. Coercive encoding behavior (similar to what * is done for CesiumPropertyTableProperty) could be added in the future. */ -static void writeTo3DTexture( - UTexture* pTexture, - FCesiumTextureResource* pResource, - const std::byte* pData, - FUpdateTextureRegion3D updateRegion, - uint32 texelSizeBytes) { - if (!pResource || !pData) +// static void directCopyTo3dTexture( +// UTexture* pTexture, +// FCesiumTextureResource* pResource, +// const std::byte* pData, +// FUpdateTextureRegion3D updateRegion, +// uint32 texelSizeBytes) { +// if (!pResource || !pData) +// return; +// +// ENQUEUE_RENDER_COMMAND(Cesium_CopyVoxels) +// ([pTexture, pResource, pData, updateRegion, texelSizeBytes]( +// FRHICommandListImmediate& RHICmdList) { +// if (!IsValid(pTexture)) { +// return; +// } +// +// // Pitch = size in bytes of each row of the source image. +// uint32 srcRowPitch = updateRegion.Width * texelSizeBytes; +// uint32 srcDepthPitch = +// updateRegion.Width * updateRegion.Height * texelSizeBytes; +// +// RHIUpdateTexture3D( +// pResource->TextureRHI, +// 0, +// updateRegion, +// srcRowPitch, +// srcDepthPitch, +// (const uint8*)(pData)); +// }); +// } + +/*static*/ void FVoxelDataTextures::writeToTexture( + const FCesiumPropertyAttributeProperty& property, + const FVoxelDataTextures::TextureData& data, + const FUpdateTextureRegion3D& updateRegion) { + if (!data.pResource || !data.pTexture) return; + uint32 texelSizeBytes = + data.encodedFormat.channels * data.encodedFormat.bytesPerChannel; + ENQUEUE_RENDER_COMMAND(Cesium_CopyVoxels) - ([pTexture, pResource, pData, updateRegion, texelSizeBytes]( - FRHICommandListImmediate& RHICmdList) { + ([pTexture = data.pTexture, + pResource = data.pResource, + format = data.encodedFormat.format, + &property, + updateRegion, + texelSizeBytes](FRHICommandListImmediate& RHICmdList) { + // We're trusting that Cesium3DTileset will destroy its attached + // CesiumVoxelRendererComponent (and thus the VoxelDataTextures) + // before unloading glTFs. As long as the texture is valid, so is the + // CesiumPropertyAttributeProperty. if (!IsValid(pTexture)) { return; } - // Pitch = size in bytes of each row of the source image. - uint32 srcRowPitch = updateRegion.Width * texelSizeBytes; - uint32 srcDepthPitch = - updateRegion.Width * updateRegion.Height * texelSizeBytes; - - RHIUpdateTexture3D( - pResource->TextureRHI, - 0, - updateRegion, - srcRowPitch, - srcDepthPitch, - (const uint8*)(pData)); + FUpdateTexture3DData UpdateData = + RHIBeginUpdateTexture3D(pResource->TextureRHI, 0, updateRegion); + + for (uint32 z = 0; z < updateRegion.Depth; z++) { + for (uint32 y = 0; y < updateRegion.Height; y++) { + int64 sourceIndex = int64( + z * updateRegion.Width * updateRegion.Height + + y * updateRegion.Width); + uint8* pDestRow = UpdateData.Data + z * UpdateData.DepthPitch + + y * UpdateData.RowPitch; + + for (uint32 x = 0; x < updateRegion.Width; x++) { + FCesiumMetadataValue rawValue = + UCesiumPropertyAttributePropertyBlueprintLibrary::GetRawValue( + property, + sourceIndex++); + + float* pFloat = + reinterpret_cast(pDestRow + x * texelSizeBytes); + FMemory::Memcpy( + *pFloat, + UCesiumMetadataValueBlueprintLibrary::GetFloat(rawValue, 0.0f)); + } + } + } + + RHIEndUpdateTexture3D(UpdateData); }); } -int64 FVoxelDataTextures::Add(const UCesiumGltfVoxelComponent& voxelComponent) { - uint32 slotIndex = ReserveNextSlot(); +int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { + uint32 slotIndex = this->reserveNextSlot(); if (slotIndex < 0) { return -1; } @@ -308,28 +370,23 @@ int64 FVoxelDataTextures::Add(const UCesiumGltfVoxelComponent& voxelComponent) { uint32 index = static_cast(slotIndex); for (auto PropertyIt : this->_propertyMap) { - const ValidatedVoxelBuffer* pValidBuffer = - voxelComponent.attributeBuffers.Find(PropertyIt.Key); - if (!pValidBuffer) { + const FCesiumPropertyAttributeProperty& property = + UCesiumPropertyAttributeBlueprintLibrary::FindProperty( + voxelComponent.PropertyAttribute, + PropertyIt.Key); + + if (UCesiumPropertyAttributePropertyBlueprintLibrary:: + GetPropertyAttributePropertyStatus(property) != + ECesiumPropertyAttributePropertyStatus::Valid) { continue; } - uint32 texelSizeBytes = PropertyIt.Value.encodedFormat.bytesPerChannel * - PropertyIt.Value.encodedFormat.channels; - const std::byte* pData = pValidBuffer->pBuffer->cesium.data.data(); - pData += pValidBuffer->pBufferView->byteOffset; - - writeTo3DTexture( - PropertyIt.Value.pTexture, - PropertyIt.Value.pResource, - pData, - updateRegion, - texelSizeBytes); + writeToTexture(property, PropertyIt.Value, updateRegion); } return slotIndex; } -int64 FVoxelDataTextures::ReserveNextSlot() { +int64 FVoxelDataTextures::reserveNextSlot() { // Remove head from list of empty slots FVoxelDataTextures::Slot* pSlot = this->_pEmptySlotsHead; if (!pSlot) { @@ -352,7 +409,7 @@ int64 FVoxelDataTextures::ReserveNextSlot() { return pSlot->Index; } -bool FVoxelDataTextures::Release(uint32 slotIndex) { +bool FVoxelDataTextures::release(uint32 slotIndex) { if (slotIndex >= this->_slots.size()) { return false; // Index out of bounds } diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.h b/Source/CesiumRuntime/Private/VoxelDataTextures.h index f3e320001..9a58c33ed 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.h +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.h @@ -54,56 +54,50 @@ class FVoxelDataTextures { * Gets the maximum number of tiles that can be added to the data * textures. Equivalent to the maximum number of data slots. */ - uint32 GetMaximumTileCount() const { + uint32 getMaximumTileCount() const { return static_cast(this->_slots.size()); } /** * Gets the number of tiles along each dimension of the textures. */ - glm::uvec3 GetTileCountAlongAxes() const { return this->_tileCountAlongAxes; } + glm::uvec3 getTileCountAlongAxes() const { return this->_tileCountAlongAxes; } /** * Retrieves the texture containing the data for the attribute with * the given ID. Returns nullptr if the attribute does not exist. */ - UTexture* GetDataTexture(const FString& attributeId) const { - const PropertyData* pProperty = this->_propertyMap.Find(attributeId); - if (pProperty) { - return pProperty->pTexture; - } - return nullptr; - } + UTexture* getTexture(const FString& attributeId) const; /** * @brief Retrieves how many data textures exist. */ - int32 GetTextureCount() const { return this->_propertyMap.Num(); } + int32 getTextureCount() const { return this->_propertyMap.Num(); } /** * Whether or not all slots in the textures are occupied. */ - bool IsFull() const { return this->_pEmptySlotsHead == nullptr; } + bool isFull() const { return this->_pEmptySlotsHead == nullptr; } /** * Attempts to add the voxel tile to the data textures. * * @returns The index of the reserved slot, or -1 if none were available. */ - int64_t Add(const UCesiumGltfVoxelComponent& voxelComponent); + int64_t add(const UCesiumGltfVoxelComponent& voxelComponent); /** * Reserves the next available empty slot. * * @returns The index of the reserved slot, or -1 if none were available. */ - int64 ReserveNextSlot(); + int64 reserveNextSlot(); /** * Releases the slot at the specified index, making the space available for * another voxel tile. */ - bool Release(uint32 slotIndex); + bool release(uint32 slotIndex); private: /** @@ -118,7 +112,7 @@ class FVoxelDataTextures { Slot* Previous = nullptr; }; - struct PropertyData { + struct TextureData { /** * The texture format used to store encoded property values. */ @@ -136,6 +130,11 @@ class FVoxelDataTextures { FCesiumTextureResource* pResource; }; + static void writeToTexture( + const FCesiumPropertyAttributeProperty& property, + const TextureData& data, + const FUpdateTextureRegion3D& region); + std::vector _slots; Slot* _pEmptySlotsHead; Slot* _pOccupiedSlotsHead; @@ -144,5 +143,5 @@ class FVoxelDataTextures { glm::uvec3 _tileCountAlongAxes; uint32 _maximumTileCount; - TMap _propertyMap; + TMap _propertyMap; }; diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index 9bb841506..bf284f397 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -79,7 +79,7 @@ FTextureRHIRef FCesiumVoxelOctreeTextureResource::InitializeTextureRHI() { .SetClearValue(createInfo.ClearValueBinding)); } -void UCesiumVoxelOctreeTexture::Update(const std::vector& data) { +void UCesiumVoxelOctreeTexture::update(const std::vector& data) { if (!this->_pResource || !this->_pResource->TextureRHI) { return; } @@ -124,7 +124,7 @@ void UCesiumVoxelOctreeTexture::Update(const std::vector& data) { } /*static*/ UCesiumVoxelOctreeTexture* -UCesiumVoxelOctreeTexture::Create(uint32 Width, uint32 MaximumTileCount) { +UCesiumVoxelOctreeTexture::create(uint32 Width, uint32 MaximumTileCount) { uint32 TilesPerRow = Width / TexelsPerNode; float Height = (float)MaximumTileCount / (float)TilesPerRow; Height = static_cast(FMath::CeilToInt64(Height)); @@ -195,8 +195,8 @@ FVoxelOctree::~FVoxelOctree() { } } -void FVoxelOctree::InitializeTexture(uint32 Width, uint32 MaximumTileCount) { - this->_pTexture = UCesiumVoxelOctreeTexture::Create(Width, MaximumTileCount); +void FVoxelOctree::initializeTexture(uint32 Width, uint32 MaximumTileCount) { + this->_pTexture = UCesiumVoxelOctreeTexture::create(Width, MaximumTileCount); FTextureResource* pResource = this->_pTexture ? this->_pTexture->GetResource() : nullptr; if (!pResource) { @@ -216,16 +216,18 @@ void FVoxelOctree::InitializeTexture(uint32 Width, uint32 MaximumTileCount) { }); } +const FVoxelOctree::Node* +FVoxelOctree::getNode(const CesiumGeometry::OctreeTileID& TileID) const { + return this->_nodes.contains(TileID) ? &this->_nodes.at(TileID) : nullptr; +} + FVoxelOctree::Node* -FVoxelOctree::GetNode(const CesiumGeometry::OctreeTileID& TileID) const { - if (this->_nodes.find(TileID) != this->_nodes.end()) { - return const_cast(&this->_nodes.at(TileID)); - } - return nullptr; +FVoxelOctree::getNode(const CesiumGeometry::OctreeTileID& TileID) { + return this->_nodes.contains(TileID) ? &this->_nodes.at(TileID) : nullptr; } -bool FVoxelOctree::CreateNode(const CesiumGeometry::OctreeTileID& TileID) { - FVoxelOctree::Node* pNode = this->GetNode(TileID); +bool FVoxelOctree::createNode(const CesiumGeometry::OctreeTileID& TileID) { + FVoxelOctree::Node* pNode = this->getNode(TileID); if (pNode) { return false; } @@ -240,7 +242,7 @@ bool FVoxelOctree::CreateNode(const CesiumGeometry::OctreeTileID& TileID) { bool foundExistingParent = false; for (uint32_t level = TileID.level; level > 0; level--) { CesiumGeometry::OctreeTileID parentTileID = - *ComputeParentTileID(currentTileID); + *computeParentTileID(currentTileID); if (this->_nodes.find(parentTileID) == this->_nodes.end()) { // Parent doesn't exist, so create it. this->_nodes.insert({parentTileID, Node()}); @@ -249,20 +251,20 @@ bool FVoxelOctree::CreateNode(const CesiumGeometry::OctreeTileID& TileID) { } FVoxelOctree::Node* pParent = &this->_nodes[parentTileID]; - pNode->Parent = pParent; + pNode->pParent = pParent; // The parent *shouldn't* have children at this point. Otherwise, our // target node would have already been found. std::array childIds = - ComputeChildTileIDs(parentTileID); + computeChildTileIDs(parentTileID); for (CesiumGeometry::OctreeTileID& childId : childIds) { if (this->_nodes.find(childId) == this->_nodes.end()) { this->_nodes.insert({childId, Node()}); - this->_nodes[childId].Parent = pParent; + this->_nodes[childId].pParent = pParent; } } - pParent->HasChildren = true; + pParent->hasChildren = true; if (foundExistingParent) { // The parent already existed in the tree previously, no need to create // its ancestors. @@ -276,12 +278,12 @@ bool FVoxelOctree::CreateNode(const CesiumGeometry::OctreeTileID& TileID) { return true; } -bool FVoxelOctree::RemoveNode(const CesiumGeometry::OctreeTileID& TileID) { +bool FVoxelOctree::removeNode(const CesiumGeometry::OctreeTileID& TileID) { if (TileID.level == 0) { return false; } - if (IsNodeRenderable(TileID)) { + if (isNodeRenderable(TileID)) { return false; } @@ -292,14 +294,14 @@ bool FVoxelOctree::RemoveNode(const CesiumGeometry::OctreeTileID& TileID) { // There may be cases where the children rely on the parent for rendering. // If so, the node's data cannot be easily released. // TODO: can you also attempt to destroy the node? - CesiumGeometry::OctreeTileID parentTileID = *ComputeParentTileID(TileID); + CesiumGeometry::OctreeTileID parentTileID = *computeParentTileID(TileID); std::array siblingIds = - ComputeChildTileIDs(parentTileID); + computeChildTileIDs(parentTileID); for (const CesiumGeometry::OctreeTileID& siblingId : siblingIds) { if (siblingId == TileID) continue; - if (IsNodeRenderable(siblingId)) { + if (isNodeRenderable(siblingId)) { hasRenderableSiblings = true; break; } @@ -315,27 +317,27 @@ bool FVoxelOctree::RemoveNode(const CesiumGeometry::OctreeTileID& TileID) { for (const CesiumGeometry::OctreeTileID& siblingId : siblingIds) { this->_nodes.erase(this->_nodes.find(siblingId)); } - this->GetNode(parentTileID)->HasChildren = false; + this->getNode(parentTileID)->hasChildren = false; // Continue to recursively remove parent nodes as long as they aren't // renderable either. - RemoveNode(parentTileID); + removeNode(parentTileID); return true; } -bool FVoxelOctree::IsNodeRenderable( +bool FVoxelOctree::isNodeRenderable( const CesiumGeometry::OctreeTileID& TileID) const { - FVoxelOctree::Node* pNode = this->GetNode(TileID); + const FVoxelOctree::Node* pNode = this->getNode(TileID); if (!pNode) { return false; } - return pNode->DataSlotIndex >= 0 || pNode->HasChildren; + return pNode->dataSlotIndex >= 0 || pNode->hasChildren; } /*static*/ std::array -FVoxelOctree::ComputeChildTileIDs(const CesiumGeometry::OctreeTileID& TileID) { +FVoxelOctree::computeChildTileIDs(const CesiumGeometry::OctreeTileID& TileID) { uint32 level = TileID.level + 1; uint32 x = TileID.x << 1; uint32 y = TileID.y << 1; @@ -353,7 +355,7 @@ FVoxelOctree::ComputeChildTileIDs(const CesiumGeometry::OctreeTileID& TileID) { } /*static*/ std::optional -FVoxelOctree::ComputeParentTileID(const CesiumGeometry::OctreeTileID& TileID) { +FVoxelOctree::computeParentTileID(const CesiumGeometry::OctreeTileID& TileID) { if (TileID.level == 0) { return std::nullopt; } @@ -392,7 +394,7 @@ void FVoxelOctree::encodeNode( constexpr uint32 TexelsPerNode = UCesiumVoxelOctreeTexture::TexelsPerNode; Node* pNode = &this->_nodes[tileId]; - if (pNode->HasChildren) { + if (pNode->hasChildren) { // Point the parent and child octree indices at each other insertNodeData( nodeData, @@ -411,7 +413,7 @@ void FVoxelOctree::encodeNode( parentTextureIndex = parentOctreeIndex * TexelsPerNode + 1; std::array childIds = - ComputeChildTileIDs(tileId); + computeChildTileIDs(tileId); for (uint32 i = 0; i < childIds.size(); i++) { octreeIndex = nodeCount; textureIndex = octreeIndex * TexelsPerNode; @@ -431,22 +433,22 @@ void FVoxelOctree::encodeNode( uint16 value = 0; uint16 levelDifference = 0; - if (pNode->DataSlotIndex >= 0) { + if (pNode->dataSlotIndex >= 0) { flag = ENodeFlag::Leaf; - value = static_cast(pNode->DataSlotIndex); - } else if (pNode->Parent) { - FVoxelOctree::Node* pParent = pNode->Parent; + value = static_cast(pNode->dataSlotIndex); + } else if (pNode->pParent) { + FVoxelOctree::Node* pParent = pNode->pParent; for (uint32 levelsAbove = 1; levelsAbove <= tileId.level; levelsAbove++) { - if (pParent->DataSlotIndex >= 0) { + if (pParent->dataSlotIndex >= 0) { flag = ENodeFlag::Leaf; - value = static_cast(pParent->DataSlotIndex); + value = static_cast(pParent->dataSlotIndex); levelDifference = levelsAbove; break; } // Continue trying to find a renderable ancestor. - pParent = pParent->Parent; + pParent = pParent->pParent; if (!pParent) { // This happens if we've reached the root node and it's not @@ -460,7 +462,7 @@ void FVoxelOctree::encodeNode( } } -void FVoxelOctree::UpdateTexture() { +void FVoxelOctree::updateTexture() { if (!this->_pTexture) { return; } @@ -477,7 +479,7 @@ void FVoxelOctree::UpdateTexture() { 0); // Pad the data as necessary for the texture copy. - uint32 regionWidth = this->_pTexture->GetTilesPerRow() * + uint32 regionWidth = this->_pTexture->getTilesPerRow() * UCesiumVoxelOctreeTexture::TexelsPerNode * sizeof(uint32); uint32 regionHeight = @@ -487,5 +489,5 @@ void FVoxelOctree::UpdateTexture() { this->_octreeData.resize(expectedSize, std::byte(0)); } - this->_pTexture->Update(this->_octreeData); + this->_pTexture->update(this->_octreeData); } diff --git a/Source/CesiumRuntime/Private/VoxelOctree.h b/Source/CesiumRuntime/Private/VoxelOctree.h index 7a3642c95..dc609a4b4 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.h +++ b/Source/CesiumRuntime/Private/VoxelOctree.h @@ -22,14 +22,14 @@ class UCesiumVoxelOctreeTexture : public UTexture2D { static const uint32 TexelsPerNode = 9; static UCesiumVoxelOctreeTexture* - Create(uint32 Width, uint32 MaximumTileCount); + create(uint32 Width, uint32 MaximumTileCount); /** * Updates the octree texture with the new input data. */ - void Update(const std::vector& data); + void update(const std::vector& data); - uint32_t GetTilesPerRow() const { return this->_tilesPerRow; } + uint32_t getTilesPerRow() const { return this->_tilesPerRow; } private: FCesiumTextureResource* _pResource; @@ -46,16 +46,16 @@ class FVoxelOctree { * @brief A representation of a tile in an implicitly tiled octree. */ struct Node { - bool HasChildren; - double LastKnownScreenSpaceError; - int64_t DataSlotIndex; - Node* Parent; + Node* pParent; + bool hasChildren; + double lastKnownScreenSpaceError; + int64_t dataSlotIndex; Node() - : HasChildren(false), - LastKnownScreenSpaceError(0.0), - DataSlotIndex(-1), - Parent(nullptr) {} + : pParent(nullptr), + hasChildren(false), + lastKnownScreenSpaceError(0.0), + dataSlotIndex(-1) {} }; /** @@ -105,7 +105,15 @@ class FVoxelOctree { * * @param TileID The octree tile ID. */ - Node* GetNode(const CesiumGeometry::OctreeTileID& TileID) const; + const Node* getNode(const CesiumGeometry::OctreeTileID& TileID) const; + + /** + * @brief Gets a node in the octree at the specified tile ID. Returns nullptr + * if it does not exist. + * + * @param TileID The octree tile ID. + */ + Node* getNode(const CesiumGeometry::OctreeTileID& TileID); /** * @brief Creates a node in the octree at the specified tile ID, including the @@ -116,7 +124,7 @@ class FVoxelOctree { * @param TileID The octree tile ID. * @param Whether the node was successfully added. */ - bool CreateNode(const CesiumGeometry::OctreeTileID& TileID); + bool createNode(const CesiumGeometry::OctreeTileID& TileID); /** * @brief Attempts to remove the node at the specified tile ID. @@ -128,16 +136,26 @@ class FVoxelOctree { * @param TileID The octree tile ID. * @return Whether the node was successfully removed. */ - bool RemoveNode(const CesiumGeometry::OctreeTileID& TileID); + bool removeNode(const CesiumGeometry::OctreeTileID& TileID); + + bool isNodeRenderable(const CesiumGeometry::OctreeTileID& TileID) const; - bool IsNodeRenderable(const CesiumGeometry::OctreeTileID& TileID) const; + void initializeTexture(uint32 width, uint32 maximumTileCount); + /** + * @brief Retrieves the texture containing the encoded octree. + */ + UTexture2D* getTexture() const { return this->_pTexture; } + + void updateTexture(); + +private: /** * @brief Retrieves the tile IDs for the children of the given tile. Does not * validate whether these exist in the octree. */ static std::array - ComputeChildTileIDs(const CesiumGeometry::OctreeTileID& TileID); + computeChildTileIDs(const CesiumGeometry::OctreeTileID& TileID); /** * @brief Retrieves the tile ID for the parent of the given tile. Does not @@ -147,18 +165,8 @@ class FVoxelOctree { * tile (i.e., level 0). */ static std::optional - ComputeParentTileID(const CesiumGeometry::OctreeTileID& TileID); - - void InitializeTexture(uint32 Width, uint32 MaximumTileCount); - - /** - * @brief Retrieves the texture containing the encoded octree. - */ - UTexture2D* GetTexture() const { return this->_pTexture; } + computeParentTileID(const CesiumGeometry::OctreeTileID& TileID); - void UpdateTexture(); - -private: /** * @brief Inserts the input values to the data vector, automatically * expanding it if the target index is out-of-bounds. diff --git a/Source/CesiumRuntime/Private/VoxelResources.cpp b/Source/CesiumRuntime/Private/VoxelResources.cpp index 499e9de31..35c1ce959 100644 --- a/Source/CesiumRuntime/Private/VoxelResources.cpp +++ b/Source/CesiumRuntime/Private/VoxelResources.cpp @@ -32,15 +32,15 @@ FVoxelResources::FVoxelResources( _loadedNodeIds(), _visibleTileQueue() { uint32 width = MaximumOctreeTextureWidth; - uint32 maximumTileCount = this->_dataTextures.GetMaximumTileCount(); - this->_octree.InitializeTexture(width, maximumTileCount); + uint32 maximumTileCount = this->_dataTextures.getMaximumTileCount(); + this->_octree.initializeTexture(width, maximumTileCount); this->_loadedNodeIds.reserve(maximumTileCount); } FVoxelResources::~FVoxelResources() {} FVector FVoxelResources::GetTileCount() const { - auto tileCount = this->_dataTextures.GetTileCountAlongAxes(); + auto tileCount = this->_dataTextures.getTileCountAlongAxes(); return FVector(tileCount.x, tileCount.y, tileCount.z); } @@ -75,7 +75,7 @@ void forEachRenderableVoxelTile(const auto& tiles, Func&& f) { for (USceneComponent* pChild : Children) { UCesiumGltfVoxelComponent* pVoxelComponent = Cast(pChild); - if (!pVoxelComponent || pVoxelComponent->attributeBuffers.IsEmpty()) { + if (!pVoxelComponent) { continue; } @@ -97,9 +97,9 @@ void FVoxelResources::Update( size_t index, const UCesiumGltfVoxelComponent* pVoxel) { double sse = VisibleTileScreenSpaceErrors[index]; - FVoxelOctree::Node* pNode = octree.GetNode(pVoxel->tileId); + FVoxelOctree::Node* pNode = octree.getNode(pVoxel->TileId); if (pNode) { - pNode->LastKnownScreenSpaceError = sse; + pNode->lastKnownScreenSpaceError = sse; } // Don't create the missing node just yet? It may not be added to the @@ -118,22 +118,22 @@ void FVoxelResources::Update( [&octree = this->_octree]( const CesiumGeometry::OctreeTileID& lhs, const CesiumGeometry::OctreeTileID& rhs) { - FVoxelOctree::Node* pLeft = octree.GetNode(lhs); - FVoxelOctree::Node* pRight = octree.GetNode(rhs); + const FVoxelOctree::Node* pLeft = octree.getNode(lhs); + const FVoxelOctree::Node* pRight = octree.getNode(rhs); if (!pLeft) { return false; } if (!pRight) { return true; } - return computePriority(pLeft->LastKnownScreenSpaceError) > - computePriority(pRight->LastKnownScreenSpaceError); + return computePriority(pLeft->lastKnownScreenSpaceError) > + computePriority(pRight->lastKnownScreenSpaceError); }); bool shouldUpdateOctree = false; // It is possible for the data textures to not exist (e.g., the default voxel // material), so check this explicitly. - bool dataTexturesExist = this->_dataTextures.GetTextureCount() > 0; + bool dataTexturesExist = this->_dataTextures.getTextureCount() > 0; size_t existingNodeCount = this->_loadedNodeIds.size(); size_t destroyedNodeCount = 0; @@ -144,9 +144,9 @@ void FVoxelResources::Update( for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); const CesiumGeometry::OctreeTileID& currentTileId = - currentTile.pComponent->tileId; - FVoxelOctree::Node* pNode = this->_octree.GetNode(currentTileId); - if (pNode && pNode->DataSlotIndex >= 0) { + currentTile.pComponent->TileId; + FVoxelOctree::Node* pNode = this->_octree.getNode(currentTileId); + if (pNode && pNode->dataSlotIndex >= 0) { // Node has already been loaded into the data textures. continue; } @@ -154,7 +154,7 @@ void FVoxelResources::Update( // Otherwise, check that the data textures have the space to add it. const UCesiumGltfVoxelComponent* pVoxel = currentTile.pComponent; size_t addNodeIndex = 0; - if (this->_dataTextures.IsFull()) { + if (this->_dataTextures.isFull()) { addNodeIndex = existingNodeCount - 1 - destroyedNodeCount; if (addNodeIndex >= this->_loadedNodeIds.size()) { // This happens when all of the previously loaded nodes have been @@ -167,28 +167,28 @@ void FVoxelResources::Update( const CesiumGeometry::OctreeTileID& lowestPriorityId = this->_loadedNodeIds[addNodeIndex]; FVoxelOctree::Node* pLowestPriorityNode = - this->_octree.GetNode(lowestPriorityId); + this->_octree.getNode(lowestPriorityId); // Release the data slot of the lowest priority node. - this->_dataTextures.Release(pLowestPriorityNode->DataSlotIndex); - pLowestPriorityNode->DataSlotIndex = -1; + this->_dataTextures.release(pLowestPriorityNode->dataSlotIndex); + pLowestPriorityNode->dataSlotIndex = -1; // Attempt to remove the node and simplify the octree. // Will not succeed if the node's siblings are renderable, or if this // node contains renderable children. - shouldUpdateOctree |= this->_octree.RemoveNode(lowestPriorityId); + shouldUpdateOctree |= this->_octree.removeNode(lowestPriorityId); } else { addNodeIndex = existingNodeCount + addedNodeCount; addedNodeCount++; } // Create the node if it does not already exist in the tree. - bool createdNewNode = this->_octree.CreateNode(currentTileId); - pNode = this->_octree.GetNode(currentTileId); - pNode->LastKnownScreenSpaceError = currentTile.sse; + bool createdNewNode = this->_octree.createNode(currentTileId); + pNode = this->_octree.getNode(currentTileId); + pNode->lastKnownScreenSpaceError = currentTile.sse; - pNode->DataSlotIndex = this->_dataTextures.Add(*pVoxel); - bool addedToDataTexture = (pNode->DataSlotIndex >= 0); + pNode->dataSlotIndex = this->_dataTextures.add(*pVoxel); + bool addedToDataTexture = (pNode->dataSlotIndex >= 0); shouldUpdateOctree |= createdNewNode || addedToDataTexture; if (!addedToDataTexture) { @@ -204,20 +204,20 @@ void FVoxelResources::Update( for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); const CesiumGeometry::OctreeTileID& currentTileId = - currentTile.pComponent->tileId; + currentTile.pComponent->TileId; // Create the node if it does not already exist in the tree. - shouldUpdateOctree |= this->_octree.CreateNode(currentTileId); + shouldUpdateOctree |= this->_octree.createNode(currentTileId); - FVoxelOctree::Node* pNode = this->_octree.GetNode(currentTileId); - pNode->LastKnownScreenSpaceError = currentTile.sse; + FVoxelOctree::Node* pNode = this->_octree.getNode(currentTileId); + pNode->lastKnownScreenSpaceError = currentTile.sse; // Set to arbitrary index. This will prompt the tile to render even though // it does not actually have data. - pNode->DataSlotIndex = 0; + pNode->dataSlotIndex = 0; } } if (shouldUpdateOctree) { - this->_octree.UpdateTexture(); + this->_octree.updateTexture(); } } diff --git a/Source/CesiumRuntime/Private/VoxelResources.h b/Source/CesiumRuntime/Private/VoxelResources.h index 7506be369..36a04ffab 100644 --- a/Source/CesiumRuntime/Private/VoxelResources.h +++ b/Source/CesiumRuntime/Private/VoxelResources.h @@ -66,14 +66,14 @@ class FVoxelResources { /** * @brief Retrieves the texture containing the encoded octree. */ - UTexture2D* GetOctreeTexture() const { return this->_octree.GetTexture(); } + UTexture2D* GetOctreeTexture() const { return this->_octree.getTexture(); } /** * @brief Retrieves the texture containing the data for the attribute with * the given ID. Returns nullptr if the attribute does not exist. */ UTexture* GetDataTexture(const FString& attributeId) const { - return this->_dataTextures.GetDataTexture(attributeId); + return this->_dataTextures.getTexture(attributeId); } /** diff --git a/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h b/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h index 8c1ae86a3..5a0f0f1d1 100644 --- a/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h +++ b/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h @@ -253,10 +253,9 @@ struct CESIUMRUNTIME_API FCesiumPropertyAttributePropertyDescription { /** * Describes how the property will be encoded as data on the GPU, if possible. - * TODO: Make this EditAnywhere once coercive encoding is supported. */ - UPROPERTY() - FCesiumMetadataEncodingDetails EncodingDetails; + UPROPERTY(EditAnywhere, Category = "Cesium") + FCesiumMetadataEncodingDetails EncodingDetails; }; /** From 714a0b6883578679cb667caedd0f406fc0a953d8 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 12 Jun 2025 17:51:15 -0400 Subject: [PATCH 13/34] Implement direct copy method --- .../CesiumPropertyAttributeProperty.cpp | 24 ++++- .../Private/VoxelDataTextures.cpp | 89 +++++++++++-------- .../CesiumRuntime/Private/VoxelDataTextures.h | 7 +- .../Public/CesiumPropertyAttributeProperty.h | 13 ++- extern/cesium-native | 2 +- 5 files changed, 93 insertions(+), 42 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp b/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp index 8231e19a7..15c198c1d 100644 --- a/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp +++ b/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp @@ -1,10 +1,11 @@ // Copyright 2020-2024 CesiumGS, Inc. and Contributors #include "CesiumPropertyAttributeProperty.h" -#include "CesiumGltf/MetadataConversions.h" -#include "CesiumGltf/PropertyTypeTraits.h" #include "CesiumMetadataEnum.h" #include "UnrealMetadataConversions.h" + +#include +#include #include namespace { @@ -382,6 +383,25 @@ TResult propertyAttributePropertyCallback( } // namespace +int64 FCesiumPropertyAttributeProperty::getAccessorStride() const { + return propertyAttributePropertyCallback( + this->_property, + this->_valueType, + this->_normalized, + [](const auto& view) -> int64 { + return view.accessorView().stride(); + }); +} +const std::byte* FCesiumPropertyAttributeProperty::getAccessorData() const { + return propertyAttributePropertyCallback( + this->_property, + this->_valueType, + this->_normalized, + [](const auto& view) -> const std::byte* { + return view.accessorView().data(); + }); +} + ECesiumPropertyAttributePropertyStatus UCesiumPropertyAttributePropertyBlueprintLibrary:: GetPropertyAttributePropertyStatus( diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp index 159faa5b5..c7615a7ec 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp @@ -141,9 +141,10 @@ FVoxelDataTextures::FVoxelDataTextures( continue; } + this->_propertyMap.Add(Property.Name, {encodedFormat, nullptr, nullptr}); + uint32 texelSizeBytes = encodedFormat.channels * encodedFormat.bytesPerChannel; - this->_propertyMap.Add(Property.Name, {encodedFormat, nullptr, nullptr}); maximumTexelSizeBytes = FMath::Max(maximumTexelSizeBytes, texelSizeBytes); } @@ -258,38 +259,7 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { * type that the texture expects. Coercive encoding behavior (similar to what * is done for CesiumPropertyTableProperty) could be added in the future. */ -// static void directCopyTo3dTexture( -// UTexture* pTexture, -// FCesiumTextureResource* pResource, -// const std::byte* pData, -// FUpdateTextureRegion3D updateRegion, -// uint32 texelSizeBytes) { -// if (!pResource || !pData) -// return; -// -// ENQUEUE_RENDER_COMMAND(Cesium_CopyVoxels) -// ([pTexture, pResource, pData, updateRegion, texelSizeBytes]( -// FRHICommandListImmediate& RHICmdList) { -// if (!IsValid(pTexture)) { -// return; -// } -// -// // Pitch = size in bytes of each row of the source image. -// uint32 srcRowPitch = updateRegion.Width * texelSizeBytes; -// uint32 srcDepthPitch = -// updateRegion.Width * updateRegion.Height * texelSizeBytes; -// -// RHIUpdateTexture3D( -// pResource->TextureRHI, -// 0, -// updateRegion, -// srcRowPitch, -// srcDepthPitch, -// (const uint8*)(pData)); -// }); -// } - -/*static*/ void FVoxelDataTextures::writeToTexture( +/*static*/ void FVoxelDataTextures::directCopyToTexture( const FCesiumPropertyAttributeProperty& property, const FVoxelDataTextures::TextureData& data, const FUpdateTextureRegion3D& updateRegion) { @@ -299,7 +269,47 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { uint32 texelSizeBytes = data.encodedFormat.channels * data.encodedFormat.bytesPerChannel; - ENQUEUE_RENDER_COMMAND(Cesium_CopyVoxels) + const uint8* pData = + reinterpret_cast(property.getAccessorData()); + + ENQUEUE_RENDER_COMMAND(Cesium_DirectCopyVoxels) + ([pTexture = data.pTexture, + pResource = data.pResource, + format = data.encodedFormat.format, + &property, + updateRegion, + texelSizeBytes, + pData](FRHICommandListImmediate& RHICmdList) { + if (!IsValid(pTexture)) { + return; + } + + // Pitch = size in bytes of each row of the source image. + uint32 srcRowPitch = updateRegion.Width * texelSizeBytes; + uint32 srcDepthPitch = + updateRegion.Width * updateRegion.Height * texelSizeBytes; + + RHIUpdateTexture3D( + pResource->TextureRHI, + 0, + updateRegion, + srcRowPitch, + srcDepthPitch, + pData); + }); +} + +/*static*/ void FVoxelDataTextures::incrementalWriteToTexture( + const FCesiumPropertyAttributeProperty& property, + const FVoxelDataTextures::TextureData& data, + const FUpdateTextureRegion3D& updateRegion) { + if (!data.pResource || !data.pTexture) + return; + + uint32 texelSizeBytes = + data.encodedFormat.channels * data.encodedFormat.bytesPerChannel; + + ENQUEUE_RENDER_COMMAND(Cesium_IncrementalWriteVoxels) ([pTexture = data.pTexture, pResource = data.pResource, format = data.encodedFormat.format, @@ -319,7 +329,7 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { for (uint32 z = 0; z < updateRegion.Depth; z++) { for (uint32 y = 0; y < updateRegion.Height; y++) { - int64 sourceIndex = int64( + int64_t sourceIndex = int64_t( z * updateRegion.Width * updateRegion.Height + y * updateRegion.Width); uint8* pDestRow = UpdateData.Data + z * UpdateData.DepthPitch + @@ -381,7 +391,14 @@ int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { continue; } - writeToTexture(property, PropertyIt.Value, updateRegion); + uint32 texelSizeBytes = PropertyIt.Value.encodedFormat.channels * + PropertyIt.Value.encodedFormat.bytesPerChannel; + + if (property.getAccessorStride() == texelSizeBytes) { + directCopyToTexture(property, PropertyIt.Value, updateRegion); + } else { + incrementalWriteToTexture(property, PropertyIt.Value, updateRegion); + } } return slotIndex; } diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.h b/Source/CesiumRuntime/Private/VoxelDataTextures.h index 9a58c33ed..647265996 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.h +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.h @@ -130,7 +130,12 @@ class FVoxelDataTextures { FCesiumTextureResource* pResource; }; - static void writeToTexture( + static void directCopyToTexture( + const FCesiumPropertyAttributeProperty& property, + const TextureData& data, + const FUpdateTextureRegion3D& region); + + static void incrementalWriteToTexture( const FCesiumPropertyAttributeProperty& property, const TextureData& data, const FUpdateTextureRegion3D& region); diff --git a/Source/CesiumRuntime/Public/CesiumPropertyAttributeProperty.h b/Source/CesiumRuntime/Public/CesiumPropertyAttributeProperty.h index 9b13f47ba..70c1b5f80 100644 --- a/Source/CesiumRuntime/Public/CesiumPropertyAttributeProperty.h +++ b/Source/CesiumRuntime/Public/CesiumPropertyAttributeProperty.h @@ -12,8 +12,7 @@ #include #include #include -#include -#include +#include #include "CesiumPropertyAttributeProperty.generated.h" @@ -126,6 +125,16 @@ struct CESIUMRUNTIME_API FCesiumPropertyAttributeProperty { _normalized = Normalized; } + /** + * @brief Gets the stride of the underlying accessor. + */ + int64 getAccessorStride() const; + + /** + * @brief Gets a pointer to the first byte of the underlying accessor's data. + */ + const std::byte* getAccessorData() const; + private: ECesiumPropertyAttributePropertyStatus _status; diff --git a/extern/cesium-native b/extern/cesium-native index ad5fac591..9ed5a7aa1 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit ad5fac59131465165c091696ce93d999a17e451c +Subproject commit 9ed5a7aa1a8474e9ef657c3b67fda00db9497e5f From 3a3665ac0f4e5ca7d6624110e2b110f9ab8dd076 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 17 Jun 2025 15:45:26 -0400 Subject: [PATCH 14/34] Fold VoxelResources into component, add fences, fix pointers --- .../Private/CesiumVoxelMetadataComponent.cpp | 13 +- .../Private/CesiumVoxelRendererComponent.cpp | 215 ++++++++- .../Private/CesiumVoxelRendererComponent.h | 55 ++- .../Private/VoxelDataTextures.cpp | 147 +++--- .../CesiumRuntime/Private/VoxelDataTextures.h | 77 +-- Source/CesiumRuntime/Private/VoxelOctree.cpp | 79 ++- Source/CesiumRuntime/Private/VoxelOctree.h | 36 +- .../CesiumRuntime/Private/VoxelResources.cpp | 453 +++++++++--------- Source/CesiumRuntime/Private/VoxelResources.h | 238 ++++----- .../Public/CesiumVoxelMetadataComponent.h | 2 +- 10 files changed, 780 insertions(+), 535 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index cd8d16fdc..1ce7d4387 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -58,13 +58,13 @@ UCesiumVoxelMetadataComponent::UCesiumVoxelMetadataComponent() : UActorComponent() { // Structure to hold one-time initialization struct FConstructorStatics { - ConstructorHelpers::FObjectFinder DefaultVolumeTexture; + ConstructorHelpers::FObjectFinder DefaultVolumeTexture; FConstructorStatics() : DefaultVolumeTexture( TEXT("/Engine/EngineResources/DefaultVolumeTexture")) {} }; static FConstructorStatics ConstructorStatics; - DefaultVolumeTexture = ConstructorStatics.DefaultVolumeTexture.Object; + pDefaultVolumeTexture = ConstructorStatics.DefaultVolumeTexture.Object; #if WITH_EDITOR this->UpdateShaderPreview(); @@ -231,7 +231,7 @@ struct VoxelMetadataClassification : public MaterialNodeClassification { struct MaterialResourceLibrary { FString HlslShaderTemplate; UMaterialFunctionMaterialLayer* MaterialLayerTemplate; - UVolumeTexture* DefaultVolumeTexture; + TObjectPtr pDefaultVolumeTexture; MaterialResourceLibrary() { static FString ContentDir = IPluginManager::Get() @@ -247,7 +247,7 @@ struct MaterialResourceLibrary { bool isValid() const { return !HlslShaderTemplate.IsEmpty() && MaterialLayerTemplate && - DefaultVolumeTexture; + pDefaultVolumeTexture; } }; @@ -804,7 +804,8 @@ static void GenerateMaterialNodes( PropertyData->MaterialExpressionEditorY = NodeY; // Set the initial value to default volume texture to avoid compilation // errors with the default 2D texture. - PropertyData->Texture = ResourceLibrary.DefaultVolumeTexture; + PropertyData->Texture = + Cast(ResourceLibrary.pDefaultVolumeTexture); MaterialState.AutoGeneratedNodes.Add(PropertyData); FCustomInput& PropertyInput = RaymarchNode->Inputs.Emplace_GetRef(); @@ -978,7 +979,7 @@ void UCesiumVoxelMetadataComponent::GenerateMaterial() { } MaterialResourceLibrary ResourceLibrary; - ResourceLibrary.DefaultVolumeTexture = this->DefaultVolumeTexture; + ResourceLibrary.pDefaultVolumeTexture = this->pDefaultVolumeTexture; if (!ResourceLibrary.isValid()) { UE_LOG( LogCesium, diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 7fe7e2d1c..99509c8ff 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -59,7 +59,8 @@ void UCesiumVoxelRendererComponent::BeginDestroy() { // Reset the pointers. this->MeshComponent = nullptr; - this->_pResources.Reset(); + this->_pOctree.Reset(); +// this->_pDataTextures->BeginDestroy(); Super::BeginDestroy(); } @@ -214,7 +215,7 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( UTF8_TO_TCHAR("Octree"), EMaterialParameterAssociation::LayerParameter, 0), - pVoxelComponent->_pResources->GetOctreeTexture()); + pVoxelComponent->_pOctree->getTexture()); pVoxelMaterial->SetScalarParameterValueByInfo( FMaterialParameterInfo( UTF8_TO_TCHAR("Shape Constant"), @@ -266,7 +267,7 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( FName(PropertyName), EMaterialParameterAssociation::LayerParameter, 0), - pVoxelComponent->_pResources->GetDataTexture(Property.Name)); + pVoxelComponent->_pDataTextures->getTexture(Property.Name)); if (Property.PropertyDetails.bHasScale) { EncodedFeaturesMetadata::SetPropertyParameterValue( @@ -318,12 +319,14 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( } } + const glm::uvec3& tileCount = + pVoxelComponent->_pDataTextures->getTileCountAlongAxes(); pVoxelMaterial->SetVectorParameterValueByInfo( FMaterialParameterInfo( UTF8_TO_TCHAR("Tile Count"), EMaterialParameterAssociation::LayerParameter, 0), - pVoxelComponent->_pResources->GetTileCount()); + FVector(tileCount.x, tileCount.y, tileCount.z)); } return pVoxelMaterial; @@ -410,7 +413,7 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( const Cesium3DTiles::Class* pVoxelClass = &tilesetMetadata.schema->classes.at(voxelClassId); - assert(pVoxelClass != nullptr); + CESIUM_ASSERT(pVoxelClass != nullptr); UCesiumVoxelRendererComponent* pVoxelComponent = NewObject(pTilesetActor); @@ -454,8 +457,7 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( glm::uvec3(dataDimensions.x, dataDimensions.z, dataDimensions.y); } - uint32 requestedTextureMemory = - FVoxelResources::DefaultDataTextureMemoryBytes; + uint32 requestedTextureMemory = DefaultDataTextureMemoryBytes; // uint64_t knownTileCount = 0; // if (tilesetMetadata.metadata) { @@ -481,12 +483,22 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( // FVoxelResources::MaximumDataTextureMemoryBytes); //} - pVoxelComponent->_pResources = MakeUnique( - pDescription, - shape, - dataDimensions, - pVoxelMesh->GetScene()->GetFeatureLevel(), - requestedTextureMemory); + pVoxelComponent->_pOctree = MakeUnique(); + if (pDescription && pVoxelMesh->GetScene()) { + pVoxelComponent->_pDataTextures = MakeUnique( + pDescription, + dataDimensions, + pVoxelMesh->GetScene()->GetFeatureLevel(), + requestedTextureMemory); + } + + uint32 width = MaximumOctreeTextureWidth; + uint32 maximumTileCount = + pVoxelComponent->_pDataTextures + ? pVoxelComponent->_pDataTextures->getMaximumTileCount() + : 1; + pVoxelComponent->_pOctree->initializeTexture(width, maximumTileCount); + pVoxelComponent->_loadedNodeIds.reserve(maximumTileCount); CreateGltfOptions::CreateVoxelOptions& options = pVoxelComponent->Options; options.pTilesetExtension = &voxelExtension; @@ -513,10 +525,181 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( return pVoxelComponent; } +namespace { +template +void forEachRenderableVoxelTile(const auto& tiles, Func&& f) { + for (size_t i = 0; i < tiles.size(); i++) { + const Cesium3DTilesSelection::Tile::Pointer& pTile = tiles[i]; + if (!pTile || + pTile->getState() != Cesium3DTilesSelection::TileLoadState::Done) { + continue; + } + + const Cesium3DTilesSelection::TileContent& content = pTile->getContent(); + const Cesium3DTilesSelection::TileRenderContent* pRenderContent = + content.getRenderContent(); + if (!pRenderContent) { + continue; + } + + UCesiumGltfComponent* Gltf = static_cast( + pRenderContent->getRenderResources()); + if (!Gltf) { + // When a tile does not have render resources (i.e. a glTF), then + // the resources either have not yet been loaded or prepared, + // or the tile is from an external tileset and does not directly + // own renderable content. In both cases, the tile is ignored here. + continue; + } + + const TArray& Children = Gltf->GetAttachChildren(); + for (USceneComponent* pChild : Children) { + UCesiumGltfVoxelComponent* pVoxelComponent = + Cast(pChild); + if (!pVoxelComponent) { + continue; + } + + f(i, pVoxelComponent); + } + } +} +} // namespace + void UCesiumVoxelRendererComponent::UpdateTiles( const std::vector& VisibleTiles, const std::vector& VisibleTileScreenSpaceErrors) { - this->_pResources->Update(VisibleTiles, VisibleTileScreenSpaceErrors); + forEachRenderableVoxelTile( + VisibleTiles, + [&VisibleTileScreenSpaceErrors, + &priorityQueue = this->_visibleTileQueue, + &pOctree = this->_pOctree]( + size_t index, + const UCesiumGltfVoxelComponent* pVoxel) { + double sse = VisibleTileScreenSpaceErrors[index]; + FVoxelOctree::Node* pNode = pOctree->getNode(pVoxel->TileId); + if (pNode) { + pNode->lastKnownScreenSpaceError = sse; + } + + // Don't create the missing node just yet? It may not be added to the + // tree depending on the priority of other nodes. + priorityQueue.push({pVoxel, sse, computePriority(sse)}); + }); + + if (this->_visibleTileQueue.empty()) { + return; + } + + // Sort the existing nodes in the megatexture by highest to lowest priority. + std::sort( + this->_loadedNodeIds.begin(), + this->_loadedNodeIds.end(), + [&pOctree = this->_pOctree]( + const CesiumGeometry::OctreeTileID& lhs, + const CesiumGeometry::OctreeTileID& rhs) { + const FVoxelOctree::Node* pLeft = pOctree->getNode(lhs); + const FVoxelOctree::Node* pRight = pOctree->getNode(rhs); + if (!pLeft) { + return false; + } + if (!pRight) { + return true; + } + return computePriority(pLeft->lastKnownScreenSpaceError) > + computePriority(pRight->lastKnownScreenSpaceError); + }); + + bool shouldUpdateOctree = false; + + size_t existingNodeCount = this->_loadedNodeIds.size(); + size_t destroyedNodeCount = 0; + size_t addedNodeCount = 0; + + if (this->_pDataTextures != nullptr) { + // For all of the visible nodes... + for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { + const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); + const CesiumGeometry::OctreeTileID& currentTileId = + currentTile.pComponent->TileId; + FVoxelOctree::Node* pNode = this->_pOctree->getNode(currentTileId); + if (pNode && pNode->dataIndex >= 0) { + // Node has already been loaded into the data textures. + pNode->isDataReady = + this->_pDataTextures->isSlotLoaded(pNode->dataIndex); + continue; + } + + // Otherwise, check that the data textures have the space to add it. + const UCesiumGltfVoxelComponent* pVoxel = currentTile.pComponent; + size_t addNodeIndex = 0; + if (this->_pDataTextures->isFull()) { + addNodeIndex = existingNodeCount - 1 - destroyedNodeCount; + if (addNodeIndex >= this->_loadedNodeIds.size()) { + // This happens when all of the previously loaded nodes have been + // replaced with new ones. + continue; + } + + destroyedNodeCount++; + + const CesiumGeometry::OctreeTileID& lowestPriorityId = + this->_loadedNodeIds[addNodeIndex]; + FVoxelOctree::Node* pLowestPriorityNode = + this->_pOctree->getNode(lowestPriorityId); + + // Release the data slot of the lowest priority node. + this->_pDataTextures->release(pLowestPriorityNode->dataIndex); + pLowestPriorityNode->dataIndex = -1; + pLowestPriorityNode->isDataReady = false; + + // Attempt to remove the node and simplify the octree. + // Will not succeed if the node's siblings are renderable, or if this + // node contains renderable children. + shouldUpdateOctree |= this->_pOctree->removeNode(lowestPriorityId); + } else { + addNodeIndex = existingNodeCount + addedNodeCount; + addedNodeCount++; + } + + // Create the node if it does not already exist in the tree. + bool createdNewNode = this->_pOctree->createNode(currentTileId); + pNode = this->_pOctree->getNode(currentTileId); + pNode->lastKnownScreenSpaceError = currentTile.sse; + + pNode->dataIndex = this->_pDataTextures->add(*pVoxel); + bool addedToDataTexture = (pNode->dataIndex >= 0); + shouldUpdateOctree |= createdNewNode || addedToDataTexture; + + if (!addedToDataTexture) { + continue; + } else if (addNodeIndex < this->_loadedNodeIds.size()) { + this->_loadedNodeIds[addNodeIndex] = currentTileId; + } else { + this->_loadedNodeIds.push_back(currentTileId); + } + } + } else { + // If there are no data textures, then for all of the visible nodes... + for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { + const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); + const CesiumGeometry::OctreeTileID& currentTileId = + currentTile.pComponent->TileId; + // Create the node if it does not already exist in the tree. + shouldUpdateOctree |= this->_pOctree->createNode(currentTileId); + + FVoxelOctree::Node* pNode = this->_pOctree->getNode(currentTileId); + pNode->lastKnownScreenSpaceError = currentTile.sse; + // Set to arbitrary index. This will prompt the tile to render even though + // it does not actually have data. + pNode->dataIndex = 0; + pNode->isDataReady = true; + } + } + + if (shouldUpdateOctree) { + this->_pOctree->updateTexture(); + } } void UCesiumVoxelRendererComponent::UpdateTransformFromCesium( @@ -545,3 +728,7 @@ void UCesiumVoxelRendererComponent::UpdateTransformFromCesium( this->MeshComponent->MarkRenderTransformDirty(); } } + +double UCesiumVoxelRendererComponent::computePriority(double sse) { + return 10.0 * sse / (sse + 1.0); +} diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h index 205e8ec36..018077058 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h @@ -2,7 +2,6 @@ #pragma once -#include "VoxelResources.h" #include "Components/SceneComponent.h" #include "Components/StaticMeshComponent.h" #include "CoreMinimal.h" @@ -10,11 +9,15 @@ #include "CustomDepthParameters.h" #include "Materials/MaterialInstanceDynamic.h" #include "Templates/UniquePtr.h" +#include "VoxelDataTextures.h" #include "VoxelGridShape.h" +#include "VoxelOctree.h" #include #include #include +#include +#include #include "CesiumVoxelRendererComponent.generated.h" @@ -30,8 +33,8 @@ UCLASS() * A component that enables raycasted voxel rendering. This is only attached to * a Cesium3DTileset when it contains voxel data. * - * Unlike typical glTF meshes, voxels are rendered by raycasting in an Unreal - * material attached to a placeholder cube mesh. + * Unlike typical triangle meshes, voxels are rendered by raycasting in an + * Unreal material attached to a placeholder cube mesh. */ class UCesiumVoxelRendererComponent : public USceneComponent { GENERATED_BODY() @@ -73,6 +76,8 @@ class UCesiumVoxelRendererComponent : public USceneComponent { */ glm::dmat4x4 HighPrecisionTransform; + void UpdateTransformFromCesium(const glm::dmat4& CesiumToUnrealTransform); + /** * Updates the voxel renderer based on the newly visible tiles. * @@ -85,9 +90,14 @@ class UCesiumVoxelRendererComponent : public USceneComponent { const std::vector& VisibleTiles, const std::vector& VisibleTileScreenSpaceErrors); - void UpdateTransformFromCesium(const glm::dmat4& CesiumToUnrealTransform); - private: + /** + * Value constants taken from CesiumJS. + */ + static const uint32 MaximumOctreeTextureWidth = 2048; + static const uint32 MaximumDataTextureMemoryBytes = 512 * 1024 * 1024; + static const uint32 DefaultDataTextureMemoryBytes = 128 * 1024 * 1024; + static UMaterialInstanceDynamic* CreateVoxelMaterial( UCesiumVoxelRendererComponent* pVoxelComponent, const FVector& dimensions, @@ -98,10 +108,37 @@ class UCesiumVoxelRendererComponent : public USceneComponent { const FCesiumVoxelClassDescription* pDescription, const Cesium3DTilesSelection::BoundingVolume& boundingVolume); - /** - * The resources used to render voxels across the tileset. - */ - TUniquePtr _pResources = nullptr; + static double computePriority(double sse); + + struct VoxelTileUpdateInfo { + const UCesiumGltfVoxelComponent* pComponent; + double sse; + double priority; + }; + + struct ScreenSpaceErrorGreaterComparator { + bool + operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { + return lhs.priority > rhs.priority; + } + }; + + struct ScreenSpaceErrorLessComparator { + bool + operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { + return lhs.priority < rhs.priority; + } + }; + + using MaxPriorityQueue = std::priority_queue< + VoxelTileUpdateInfo, + std::vector, + ScreenSpaceErrorLessComparator>; + + TUniquePtr _pOctree; + TUniquePtr _pDataTextures; + std::vector _loadedNodeIds; + MaxPriorityQueue _visibleTileQueue; /** * The tileset that owns this voxel renderer. diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp index c7615a7ec..a8d0d6bc9 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp @@ -141,10 +141,12 @@ FVoxelDataTextures::FVoxelDataTextures( continue; } - this->_propertyMap.Add(Property.Name, {encodedFormat, nullptr, nullptr}); - uint32 texelSizeBytes = encodedFormat.channels * encodedFormat.bytesPerChannel; + this->_propertyMap.Add( + Property.Name, + {encodedFormat, texelSizeBytes, nullptr, nullptr}); + maximumTexelSizeBytes = FMath::Max(maximumTexelSizeBytes, texelSizeBytes); } @@ -184,9 +186,9 @@ FVoxelDataTextures::FVoxelDataTextures( this->_slots.resize(this->_maximumTileCount, Slot()); for (size_t i = 0; i < this->_slots.size(); i++) { Slot* pSlot = &_slots[i]; - pSlot->Index = static_cast(i); - pSlot->Previous = i > 0 ? &_slots[i - 1] : nullptr; - pSlot->Next = i < _slots.size() - 1 ? &_slots[i + 1] : nullptr; + pSlot->index = static_cast(i); + pSlot->pPrevious = i > 0 ? &_slots[i - 1] : nullptr; + pSlot->pNext = i < _slots.size() - 1 ? &_slots[i + 1] : nullptr; } this->_pEmptySlotsHead = &this->_slots[0]; @@ -234,20 +236,34 @@ FVoxelDataTextures::FVoxelDataTextures( } } -FVoxelDataTextures ::~FVoxelDataTextures() { - for (auto propertyIt : this->_propertyMap) { - UTexture* pTexture = propertyIt.Value.pTexture; - propertyIt.Value.pTexture = nullptr; - propertyIt.Value.pResource = nullptr; - - if (IsValid(pTexture)) { - pTexture->RemoveFromRoot(); - CesiumLifetime::destroy(pTexture); - } - } - - this->_propertyMap.Empty(); -} +FVoxelDataTextures::~FVoxelDataTextures() {} + +// void FVoxelDataTextures::BeginDestroy() { +// for (auto propertyIt : this->_propertyMap) { +// UTexture* pTexture = propertyIt.Value.pTexture; +// pTexture->BeginDestroy(); +// propertyIt.Value.pTexture = nullptr; +// propertyIt.Value.pResource = nullptr; +// +// if (IsValid(pTexture)) { +// pTexture->RemoveFromRoot(); +// CesiumLifetime::destroy(pTexture); +// } +// } +// +// this->_propertyMap.Empty(); +// +// Super::BeginDestroy(); +//} +// +// bool FVoxelDataTextures::IsReadyForFinishDestroy() { +// // for (auto propertyIt : this->_propertyMap) { +// // UTexture* pTexture = propertyIt.Value.pTexture; +// // if (pTexture && !pTexture->IsReadyForFinishDestroy()) +// // return false; +// // } +// return Super::IsReadyForFinishDestroy(); +//} UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { const TextureData* pProperty = this->_propertyMap.Find(attributeId); @@ -266,9 +282,6 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { if (!data.pResource || !data.pTexture) return; - uint32 texelSizeBytes = - data.encodedFormat.channels * data.encodedFormat.bytesPerChannel; - const uint8* pData = reinterpret_cast(property.getAccessorData()); @@ -278,7 +291,7 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { format = data.encodedFormat.format, &property, updateRegion, - texelSizeBytes, + texelSizeBytes = data.texelSizeBytes, pData](FRHICommandListImmediate& RHICmdList) { if (!IsValid(pTexture)) { return; @@ -306,16 +319,14 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { if (!data.pResource || !data.pTexture) return; - uint32 texelSizeBytes = - data.encodedFormat.channels * data.encodedFormat.bytesPerChannel; - ENQUEUE_RENDER_COMMAND(Cesium_IncrementalWriteVoxels) ([pTexture = data.pTexture, pResource = data.pResource, format = data.encodedFormat.format, &property, updateRegion, - texelSizeBytes](FRHICommandListImmediate& RHICmdList) { + texelSizeBytes = + data.texelSizeBytes](FRHICommandListImmediate& RHICmdList) { // We're trusting that Cesium3DTileset will destroy its attached // CesiumVoxelRendererComponent (and thus the VoxelDataTextures) // before unloading glTFs. As long as the texture is valid, so is the @@ -354,8 +365,16 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { }); } +bool FVoxelDataTextures::isSlotLoaded(int64 index) const { + if (index < 0 || index >= int64(this->_slots.size())) + return false; + + return this->_slots[size_t(index)].fence && + this->_slots[size_t(index)].fence->IsFenceComplete(); +} + int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { - uint32 slotIndex = this->reserveNextSlot(); + int64 slotIndex = this->reserveNextSlot(); if (slotIndex < 0) { return -1; } @@ -391,62 +410,64 @@ int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { continue; } - uint32 texelSizeBytes = PropertyIt.Value.encodedFormat.channels * - PropertyIt.Value.encodedFormat.bytesPerChannel; - - if (property.getAccessorStride() == texelSizeBytes) { + if (property.getAccessorStride() == PropertyIt.Value.texelSizeBytes) { directCopyToTexture(property, PropertyIt.Value, updateRegion); } else { incrementalWriteToTexture(property, PropertyIt.Value, updateRegion); } } + + this->_slots[slotIndex].fence.emplace().BeginFence(); + return slotIndex; } -int64 FVoxelDataTextures::reserveNextSlot() { - // Remove head from list of empty slots - FVoxelDataTextures::Slot* pSlot = this->_pEmptySlotsHead; - if (!pSlot) { - return -1; +bool FVoxelDataTextures::release(int64_t slotIndex) { + if (slotIndex < 0 || slotIndex >= int64(this->_slots.size())) { + return false; // Index out of bounds } - this->_pEmptySlotsHead = pSlot->Next; + Slot* pSlot = &this->_slots[slotIndex]; + pSlot->fence.reset(); - if (this->_pEmptySlotsHead) { - this->_pEmptySlotsHead->Previous = nullptr; + if (pSlot->pPrevious) { + pSlot->pPrevious->pNext = pSlot->pNext; + } + if (pSlot->pNext) { + pSlot->pNext->pPrevious = pSlot->pPrevious; } - // Move to list of occupied slots (as the new head) - pSlot->Next = this->_pOccupiedSlotsHead; - if (pSlot->Next) { - this->_pOccupiedSlotsHead->Previous = pSlot; + // Move to list of empty slots (as the new head) + pSlot->pNext = this->_pEmptySlotsHead; + if (pSlot->pNext) { + pSlot->pNext->pPrevious = pSlot; } - this->_pOccupiedSlotsHead = pSlot; - return pSlot->Index; + pSlot->pPrevious = nullptr; + this->_pEmptySlotsHead = pSlot; + + return true; } -bool FVoxelDataTextures::release(uint32 slotIndex) { - if (slotIndex >= this->_slots.size()) { - return false; // Index out of bounds +int64 FVoxelDataTextures::reserveNextSlot() { + // Remove head from list of empty slots + FVoxelDataTextures::Slot* pSlot = this->_pEmptySlotsHead; + if (!pSlot) { + return -1; } - Slot* pSlot = &this->_slots[slotIndex]; - if (pSlot->Previous) { - pSlot->Previous->Next = pSlot->Next; - } - if (pSlot->Next) { - pSlot->Next->Previous = pSlot->Previous; - } + this->_pEmptySlotsHead = pSlot->pNext; - // Move to list of empty slots (as the new head) - pSlot->Next = this->_pEmptySlotsHead; - if (pSlot->Next) { - pSlot->Next->Previous = pSlot; + if (this->_pEmptySlotsHead) { + this->_pEmptySlotsHead->pPrevious = nullptr; } - pSlot->Previous = nullptr; - this->_pEmptySlotsHead = pSlot; + // Move to list of occupied slots (as the new head) + pSlot->pNext = this->_pOccupiedSlotsHead; + if (pSlot->pNext) { + this->_pOccupiedSlotsHead->pPrevious = pSlot; + } + this->_pOccupiedSlotsHead = pSlot; - return true; + return pSlot->index; } diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.h b/Source/CesiumRuntime/Private/VoxelDataTextures.h index 647265996..5573e0f8c 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.h +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.h @@ -5,14 +5,10 @@ #include "CesiumCommon.h" #include "CesiumMetadataValueType.h" #include "EncodedFeaturesMetadata.h" +#include "RenderCommandFence.h" #include - #include -namespace Cesium3DTiles { -struct Class; -} - struct FCesiumVoxelClassDescription; class FCesiumTextureResource; class UCesiumGltfVoxelComponent; @@ -43,15 +39,18 @@ class FVoxelDataTextures { * voxel attribute. */ FVoxelDataTextures( - const FCesiumVoxelClassDescription* pVoxelClass, + const FCesiumVoxelClassDescription* pDescription, const glm::uvec3& dataDimensions, ERHIFeatureLevel::Type featureLevel, uint32 requestedMemoryPerTexture); ~FVoxelDataTextures(); + // virtual void BeginDestroy() override; + // virtual bool IsReadyForFinishDestroy() override; + /** - * Gets the maximum number of tiles that can be added to the data + * @brief Gets the maximum number of tiles that can be added to the data * textures. Equivalent to the maximum number of data slots. */ uint32 getMaximumTileCount() const { @@ -59,12 +58,12 @@ class FVoxelDataTextures { } /** - * Gets the number of tiles along each dimension of the textures. + * @brief Gets the number of tiles along each dimension of the textures. */ glm::uvec3 getTileCountAlongAxes() const { return this->_tileCountAlongAxes; } /** - * Retrieves the texture containing the data for the attribute with + * @brief Retrieves the texture containing the data for the attribute with * the given ID. Returns nullptr if the attribute does not exist. */ UTexture* getTexture(const FString& attributeId) const; @@ -75,71 +74,95 @@ class FVoxelDataTextures { int32 getTextureCount() const { return this->_propertyMap.Num(); } /** - * Whether or not all slots in the textures are occupied. + * @brief Whether or not all slots in the textures are occupied. */ bool isFull() const { return this->_pEmptySlotsHead == nullptr; } /** - * Attempts to add the voxel tile to the data textures. - * - * @returns The index of the reserved slot, or -1 if none were available. + * @brief Whether or not the slot at the given index is loaded. */ - int64_t add(const UCesiumGltfVoxelComponent& voxelComponent); + bool isSlotLoaded(int64 index) const; /** - * Reserves the next available empty slot. + * @brief Attempts to add the voxel tile to the data textures. * * @returns The index of the reserved slot, or -1 if none were available. */ - int64 reserveNextSlot(); + int64_t add(const UCesiumGltfVoxelComponent& voxelComponent); /** - * Releases the slot at the specified index, making the space available for - * another voxel tile. + * @brief Releases the slot at the specified index, making the space available + * for another voxel tile. */ - bool release(uint32 slotIndex); + bool release(int64_t slotIndex); private: /** - * Represents a slot in the voxel data texture that contains a single + * @brief Represents a slot in the voxel data texture that contains a single * tile's data. Slots function like nodes in a linked list in order to track * which slots are occupied with data, while preventing the need for 2 vectors * with maximum tile capacity. */ struct Slot { - int64 Index = -1; - Slot* Next = nullptr; - Slot* Previous = nullptr; + int64_t index = -1; + Slot* pNext = nullptr; + Slot* pPrevious = nullptr; + std::optional fence; }; struct TextureData { /** - * The texture format used to store encoded property values. + * @brief The texture format used to store encoded property values. */ EncodedFeaturesMetadata::EncodedPixelFormat encodedFormat; /** - * The data texture for this property. + * @brief The size of a texel in the texture, in bytes. Derived from the + * texture format. + */ + uint32 texelSizeBytes; + + /** + * @brief The data texture for this property. */ UTexture* pTexture; /** - * A pointer to the texture resource. There is no way to retrieve this - * through the UTexture API, so the pointer is stored here. + * @brief A pointer to the texture resource. There is no way to retrieve + * this through the UTexture API, so the pointer is stored here. */ FCesiumTextureResource* pResource; }; + /** + * @brief Directly copies the buffer from the given property attribute + * property to the texture. This is much faster than + * incrementalWriteToTexture, but requires the attribute data to be + * contiguous. + */ static void directCopyToTexture( const FCesiumPropertyAttributeProperty& property, const TextureData& data, const FUpdateTextureRegion3D& region); + /** + * @brief Incrementally writes the data from the given property attribute + * property to the texture. Each element is converted to uint8 or float, + * depending on the texture format, before being written to the pixel. + * Necessary for some accessor types or accessors with non-continguous data. + */ static void incrementalWriteToTexture( const FCesiumPropertyAttributeProperty& property, const TextureData& data, const FUpdateTextureRegion3D& region); + /** + * @brief Reserves the next available empty slot. + * + * @returns The index of the reserved slot, or -1 if none were available. + */ + int64_t reserveNextSlot(); + std::vector _slots; Slot* _pEmptySlotsHead; Slot* _pOccupiedSlotsHead; diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index bf284f397..442a07bf5 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -232,7 +232,7 @@ bool FVoxelOctree::createNode(const CesiumGeometry::OctreeTileID& TileID) { return false; } - // Create this node first. + // Create the target node first. this->_nodes.insert({TileID, Node()}); pNode = &this->_nodes[TileID]; @@ -243,11 +243,11 @@ bool FVoxelOctree::createNode(const CesiumGeometry::OctreeTileID& TileID) { for (uint32_t level = TileID.level; level > 0; level--) { CesiumGeometry::OctreeTileID parentTileID = *computeParentTileID(currentTileID); - if (this->_nodes.find(parentTileID) == this->_nodes.end()) { + if (this->_nodes.contains(parentTileID)) { + foundExistingParent = true; + } else { // Parent doesn't exist, so create it. this->_nodes.insert({parentTileID, Node()}); - } else { - foundExistingParent = true; } FVoxelOctree::Node* pParent = &this->_nodes[parentTileID]; @@ -255,12 +255,11 @@ bool FVoxelOctree::createNode(const CesiumGeometry::OctreeTileID& TileID) { // The parent *shouldn't* have children at this point. Otherwise, our // target node would have already been found. - std::array childIds = - computeChildTileIDs(parentTileID); - for (CesiumGeometry::OctreeTileID& childId : childIds) { - if (this->_nodes.find(childId) == this->_nodes.end()) { - this->_nodes.insert({childId, Node()}); - this->_nodes[childId].pParent = pParent; + for (const CesiumGeometry::OctreeTileID& child : + ImplicitTilingUtilities::getChildren(parentTileID)) { + if (!this->_nodes.contains(child)) { + this->_nodes.insert({child, Node()}); + this->_nodes[child].pParent = pParent; } } @@ -278,12 +277,12 @@ bool FVoxelOctree::createNode(const CesiumGeometry::OctreeTileID& TileID) { return true; } -bool FVoxelOctree::removeNode(const CesiumGeometry::OctreeTileID& TileID) { - if (TileID.level == 0) { +bool FVoxelOctree::removeNode(const CesiumGeometry::OctreeTileID& tileId) { + if (tileId.level == 0) { return false; } - if (isNodeRenderable(TileID)) { + if (isNodeRenderable(tileId)) { return false; } @@ -294,11 +293,11 @@ bool FVoxelOctree::removeNode(const CesiumGeometry::OctreeTileID& TileID) { // There may be cases where the children rely on the parent for rendering. // If so, the node's data cannot be easily released. // TODO: can you also attempt to destroy the node? - CesiumGeometry::OctreeTileID parentTileID = *computeParentTileID(TileID); - std::array siblingIds = - computeChildTileIDs(parentTileID); - for (const CesiumGeometry::OctreeTileID& siblingId : siblingIds) { - if (siblingId == TileID) + CesiumGeometry::OctreeTileID parentTileId = *computeParentTileID(tileId); + Cesium3DTilesContent::OctreeChildren siblings = + ImplicitTilingUtilities::getChildren(parentTileId); + for (const CesiumGeometry::OctreeTileID& siblingId : siblings) { + if (siblingId == tileId) continue; if (isNodeRenderable(siblingId)) { @@ -314,14 +313,14 @@ bool FVoxelOctree::removeNode(const CesiumGeometry::OctreeTileID& TileID) { } // Otherwise, okay to remove the nodes. - for (const CesiumGeometry::OctreeTileID& siblingId : siblingIds) { + for (const CesiumGeometry::OctreeTileID& siblingId : siblings) { this->_nodes.erase(this->_nodes.find(siblingId)); } - this->getNode(parentTileID)->hasChildren = false; + this->getNode(parentTileId)->hasChildren = false; // Continue to recursively remove parent nodes as long as they aren't // renderable either. - removeNode(parentTileID); + removeNode(parentTileId); return true; } @@ -333,25 +332,7 @@ bool FVoxelOctree::isNodeRenderable( return false; } - return pNode->dataSlotIndex >= 0 || pNode->hasChildren; -} - -/*static*/ std::array -FVoxelOctree::computeChildTileIDs(const CesiumGeometry::OctreeTileID& TileID) { - uint32 level = TileID.level + 1; - uint32 x = TileID.x << 1; - uint32 y = TileID.y << 1; - uint32 z = TileID.z << 1; - - return { - OctreeTileID(level, x, y, z), - OctreeTileID(level, x + 1, y, z), - OctreeTileID(level, x, y + 1, z), - OctreeTileID(level, x + 1, y + 1, z), - OctreeTileID(level, x, y, z + 1), - OctreeTileID(level, x + 1, y, z + 1), - OctreeTileID(level, x, y + 1, z + 1), - OctreeTileID(level, x + 1, y + 1, z + 1)}; + return pNode->isDataReady || pNode->hasChildren; } /*static*/ std::optional @@ -412,20 +393,20 @@ void FVoxelOctree::encodeNode( parentOctreeIndex = octreeIndex; parentTextureIndex = parentOctreeIndex * TexelsPerNode + 1; - std::array childIds = - computeChildTileIDs(tileId); - for (uint32 i = 0; i < childIds.size(); i++) { + uint32 childIndex = 0; + for (const CesiumGeometry::OctreeTileID& childId : + ImplicitTilingUtilities::getChildren(tileId)) { octreeIndex = nodeCount; textureIndex = octreeIndex * TexelsPerNode; encodeNode( - childIds[i], + childId, nodeData, nodeCount, octreeIndex, textureIndex, parentOctreeIndex, - parentTextureIndex + i); + parentTextureIndex + childIndex++); } } else { // Leaf nodes involve more complexity. @@ -433,16 +414,16 @@ void FVoxelOctree::encodeNode( uint16 value = 0; uint16 levelDifference = 0; - if (pNode->dataSlotIndex >= 0) { + if (pNode->isDataReady) { flag = ENodeFlag::Leaf; - value = static_cast(pNode->dataSlotIndex); + value = static_cast(pNode->dataIndex); } else if (pNode->pParent) { FVoxelOctree::Node* pParent = pNode->pParent; for (uint32 levelsAbove = 1; levelsAbove <= tileId.level; levelsAbove++) { - if (pParent->dataSlotIndex >= 0) { + if (pParent->isDataReady) { flag = ENodeFlag::Leaf; - value = static_cast(pParent->dataSlotIndex); + value = static_cast(pParent->dataIndex); levelDifference = levelsAbove; break; } diff --git a/Source/CesiumRuntime/Private/VoxelOctree.h b/Source/CesiumRuntime/Private/VoxelOctree.h index dc609a4b4..de501ff47 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.h +++ b/Source/CesiumRuntime/Private/VoxelOctree.h @@ -49,13 +49,15 @@ class FVoxelOctree { Node* pParent; bool hasChildren; double lastKnownScreenSpaceError; - int64_t dataSlotIndex; + int64_t dataIndex; + bool isDataReady; Node() : pParent(nullptr), hasChildren(false), lastKnownScreenSpaceError(0.0), - dataSlotIndex(-1) {} + dataIndex(-1), + isDataReady(false) {} }; /** @@ -150,13 +152,6 @@ class FVoxelOctree { void updateTexture(); private: - /** - * @brief Retrieves the tile IDs for the children of the given tile. Does not - * validate whether these exist in the octree. - */ - static std::array - computeChildTileIDs(const CesiumGeometry::OctreeTileID& TileID); - /** * @brief Retrieves the tile ID for the parent of the given tile. Does not * validate whether either tile exists in the octree. @@ -232,32 +227,31 @@ class FVoxelOctree { uint32 parentOctreeIndex, uint32 parentTextureIndex); + + struct OctreeTileIDHash { + size_t operator()(const CesiumGeometry::OctreeTileID& tileId) const; + }; + // This implementation is inspired by Linear (hashed) Octrees: // https://geidav.wordpress.com/2014/08/18/advanced-octrees-2-node-representations/ // - // First, nodes must track their parent / child relationships so that the tree - // structure can be encoded to a texture, for voxel raymarching. - // - // Second, nodes must be easily created and/or accessed. cesium-native passes - // tiles in over in a vector without spatial organization. Typical tree + // Nodes must track their parent / child relationships so that the tree + // structure can be encoded to a texture, for voxel raymarching. However, + // nodes must also be easily created and/or accessed. cesium-native passes + // tiles over in a vector without spatial organization. Typical tree // queries are O(log(n)) where n = # tree levels. This is unideal, since it's // likely that multiple tiles will be made visible in an update based on // movement. // // The compromise: a hashmap that stores octree nodes based on their tile ID. // The nodes don't point to any children themselves; instead, they store a - // bool indicating whether or not children have been created for them. It's up - // to the octree to properly manage this. - struct OctreeTileIDHash { - size_t operator()(const CesiumGeometry::OctreeTileID& tileId) const; - }; - + // bool indicating whether or not children have been created for them. It's on + // the octree to properly manage this. using NodeMap = std::unordered_map; NodeMap _nodes; UCesiumVoxelOctreeTexture* _pTexture; - // As the octree grows, save the allocated memory so that recomputing the // same-size octree won't require more allocations. std::vector _octreeData; diff --git a/Source/CesiumRuntime/Private/VoxelResources.cpp b/Source/CesiumRuntime/Private/VoxelResources.cpp index 35c1ce959..a812dba4f 100644 --- a/Source/CesiumRuntime/Private/VoxelResources.cpp +++ b/Source/CesiumRuntime/Private/VoxelResources.cpp @@ -1,226 +1,227 @@ -// Copyright 2020-2024 CesiumGS, Inc. and Contributors - -#include "VoxelResources.h" -#include "CesiumGltfComponent.h" -#include "CesiumRuntime.h" -#include "EncodedMetadataConversions.h" -#include "VoxelGridShape.h" - -#include -#include -#include -#include -#include - -#include -#include - -using namespace CesiumGltf; -using namespace Cesium3DTilesContent; - -FVoxelResources::FVoxelResources( - const FCesiumVoxelClassDescription* pVoxelClass, - EVoxelGridShape shape, - const glm::uvec3& dataDimensions, - ERHIFeatureLevel::Type featureLevel, - uint32 requestedMemoryPerDataTexture) - : _dataTextures( - pVoxelClass, - dataDimensions, - featureLevel, - requestedMemoryPerDataTexture), - _loadedNodeIds(), - _visibleTileQueue() { - uint32 width = MaximumOctreeTextureWidth; - uint32 maximumTileCount = this->_dataTextures.getMaximumTileCount(); - this->_octree.initializeTexture(width, maximumTileCount); - this->_loadedNodeIds.reserve(maximumTileCount); -} - -FVoxelResources::~FVoxelResources() {} - -FVector FVoxelResources::GetTileCount() const { - auto tileCount = this->_dataTextures.getTileCountAlongAxes(); - return FVector(tileCount.x, tileCount.y, tileCount.z); -} - -namespace { -template -void forEachRenderableVoxelTile(const auto& tiles, Func&& f) { - for (size_t i = 0; i < tiles.size(); i++) { - const Cesium3DTilesSelection::Tile::Pointer& pTile = tiles[i]; - if (!pTile || - pTile->getState() != Cesium3DTilesSelection::TileLoadState::Done) { - continue; - } - - const Cesium3DTilesSelection::TileContent& content = pTile->getContent(); - const Cesium3DTilesSelection::TileRenderContent* pRenderContent = - content.getRenderContent(); - if (!pRenderContent) { - continue; - } - - UCesiumGltfComponent* Gltf = static_cast( - pRenderContent->getRenderResources()); - if (!Gltf) { - // When a tile does not have render resources (i.e. a glTF), then - // the resources either have not yet been loaded or prepared, - // or the tile is from an external tileset and does not directly - // own renderable content. In both cases, the tile is ignored here. - continue; - } - - const TArray& Children = Gltf->GetAttachChildren(); - for (USceneComponent* pChild : Children) { - UCesiumGltfVoxelComponent* pVoxelComponent = - Cast(pChild); - if (!pVoxelComponent) { - continue; - } - - f(i, pVoxelComponent); - } - } -} -} // namespace - -void FVoxelResources::Update( - - const std::vector& VisibleTiles, - const std::vector& VisibleTileScreenSpaceErrors) { - forEachRenderableVoxelTile( - VisibleTiles, - [&VisibleTileScreenSpaceErrors, - &priorityQueue = this->_visibleTileQueue, - &octree = this->_octree]( - size_t index, - const UCesiumGltfVoxelComponent* pVoxel) { - double sse = VisibleTileScreenSpaceErrors[index]; - FVoxelOctree::Node* pNode = octree.getNode(pVoxel->TileId); - if (pNode) { - pNode->lastKnownScreenSpaceError = sse; - } - - // Don't create the missing node just yet? It may not be added to the - // tree depending on the priority of other nodes. - priorityQueue.push({pVoxel, sse, computePriority(sse)}); - }); - - if (this->_visibleTileQueue.empty()) { - return; - } - - // Sort the existing nodes in the megatexture by highest to lowest priority. - std::sort( - this->_loadedNodeIds.begin(), - this->_loadedNodeIds.end(), - [&octree = this->_octree]( - const CesiumGeometry::OctreeTileID& lhs, - const CesiumGeometry::OctreeTileID& rhs) { - const FVoxelOctree::Node* pLeft = octree.getNode(lhs); - const FVoxelOctree::Node* pRight = octree.getNode(rhs); - if (!pLeft) { - return false; - } - if (!pRight) { - return true; - } - return computePriority(pLeft->lastKnownScreenSpaceError) > - computePriority(pRight->lastKnownScreenSpaceError); - }); - - bool shouldUpdateOctree = false; - // It is possible for the data textures to not exist (e.g., the default voxel - // material), so check this explicitly. - bool dataTexturesExist = this->_dataTextures.getTextureCount() > 0; - - size_t existingNodeCount = this->_loadedNodeIds.size(); - size_t destroyedNodeCount = 0; - size_t addedNodeCount = 0; - - if (dataTexturesExist) { - // For all of the visible nodes... - for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { - const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); - const CesiumGeometry::OctreeTileID& currentTileId = - currentTile.pComponent->TileId; - FVoxelOctree::Node* pNode = this->_octree.getNode(currentTileId); - if (pNode && pNode->dataSlotIndex >= 0) { - // Node has already been loaded into the data textures. - continue; - } - - // Otherwise, check that the data textures have the space to add it. - const UCesiumGltfVoxelComponent* pVoxel = currentTile.pComponent; - size_t addNodeIndex = 0; - if (this->_dataTextures.isFull()) { - addNodeIndex = existingNodeCount - 1 - destroyedNodeCount; - if (addNodeIndex >= this->_loadedNodeIds.size()) { - // This happens when all of the previously loaded nodes have been - // replaced with new ones. - continue; - } - - destroyedNodeCount++; - - const CesiumGeometry::OctreeTileID& lowestPriorityId = - this->_loadedNodeIds[addNodeIndex]; - FVoxelOctree::Node* pLowestPriorityNode = - this->_octree.getNode(lowestPriorityId); - - // Release the data slot of the lowest priority node. - this->_dataTextures.release(pLowestPriorityNode->dataSlotIndex); - pLowestPriorityNode->dataSlotIndex = -1; - - // Attempt to remove the node and simplify the octree. - // Will not succeed if the node's siblings are renderable, or if this - // node contains renderable children. - shouldUpdateOctree |= this->_octree.removeNode(lowestPriorityId); - } else { - addNodeIndex = existingNodeCount + addedNodeCount; - addedNodeCount++; - } - - // Create the node if it does not already exist in the tree. - bool createdNewNode = this->_octree.createNode(currentTileId); - pNode = this->_octree.getNode(currentTileId); - pNode->lastKnownScreenSpaceError = currentTile.sse; - - pNode->dataSlotIndex = this->_dataTextures.add(*pVoxel); - bool addedToDataTexture = (pNode->dataSlotIndex >= 0); - shouldUpdateOctree |= createdNewNode || addedToDataTexture; - - if (!addedToDataTexture) { - continue; - } else if (addNodeIndex < this->_loadedNodeIds.size()) { - this->_loadedNodeIds[addNodeIndex] = currentTileId; - } else { - this->_loadedNodeIds.push_back(currentTileId); - } - } - } else { - // If there are no data textures, then for all of the visible nodes... - for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { - const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); - const CesiumGeometry::OctreeTileID& currentTileId = - currentTile.pComponent->TileId; - // Create the node if it does not already exist in the tree. - shouldUpdateOctree |= this->_octree.createNode(currentTileId); - - FVoxelOctree::Node* pNode = this->_octree.getNode(currentTileId); - pNode->lastKnownScreenSpaceError = currentTile.sse; - // Set to arbitrary index. This will prompt the tile to render even though - // it does not actually have data. - pNode->dataSlotIndex = 0; - } - } - - if (shouldUpdateOctree) { - this->_octree.updateTexture(); - } -} - -double FVoxelResources::computePriority(double sse) { - return 10.0 * sse / (sse + 1.0); -} +//// Copyright 2020-2024 CesiumGS, Inc. and Contributors +// +//#include "VoxelResources.h" +//#include "CesiumGltfComponent.h" +//#include "CesiumRuntime.h" +//#include "EncodedMetadataConversions.h" +//#include "VoxelGridShape.h" +// +//#include +//#include +//#include +//#include +//#include +// +//#include +//#include +// +//using namespace CesiumGltf; +//using namespace Cesium3DTilesContent; +// +//FVoxelResources::FVoxelResources( +// const FCesiumVoxelClassDescription* pVoxelClass, +// EVoxelGridShape shape, +// const glm::uvec3& dataDimensions, +// ERHIFeatureLevel::Type featureLevel, +// uint32 requestedMemoryPerDataTexture) +// : _dataTextures( +// pVoxelClass, +// dataDimensions, +// featureLevel, +// requestedMemoryPerDataTexture), +// _loadedNodeIds(), +// _visibleTileQueue() { +// uint32 width = MaximumOctreeTextureWidth; +// uint32 maximumTileCount = this->_dataTextures.getMaximumTileCount(); +// this->_octree.initializeTexture(width, maximumTileCount); +// this->_loadedNodeIds.reserve(maximumTileCount); +//} +// +//FVoxelResources::~FVoxelResources() {} +// +//FVector FVoxelResources::GetTileCount() const { +// auto tileCount = this->_dataTextures.getTileCountAlongAxes(); +// return FVector(tileCount.x, tileCount.y, tileCount.z); +//} +// +//namespace { +//template +//void forEachRenderableVoxelTile(const auto& tiles, Func&& f) { +// for (size_t i = 0; i < tiles.size(); i++) { +// const Cesium3DTilesSelection::Tile::Pointer& pTile = tiles[i]; +// if (!pTile || +// pTile->getState() != Cesium3DTilesSelection::TileLoadState::Done) { +// continue; +// } +// +// const Cesium3DTilesSelection::TileContent& content = pTile->getContent(); +// const Cesium3DTilesSelection::TileRenderContent* pRenderContent = +// content.getRenderContent(); +// if (!pRenderContent) { +// continue; +// } +// +// UCesiumGltfComponent* Gltf = static_cast( +// pRenderContent->getRenderResources()); +// if (!Gltf) { +// // When a tile does not have render resources (i.e. a glTF), then +// // the resources either have not yet been loaded or prepared, +// // or the tile is from an external tileset and does not directly +// // own renderable content. In both cases, the tile is ignored here. +// continue; +// } +// +// const TArray& Children = Gltf->GetAttachChildren(); +// for (USceneComponent* pChild : Children) { +// UCesiumGltfVoxelComponent* pVoxelComponent = +// Cast(pChild); +// if (!pVoxelComponent) { +// continue; +// } +// +// f(i, pVoxelComponent); +// } +// } +//} +//} // namespace +// +//void FVoxelResources::Update( +// const std::vector& VisibleTiles, +// const std::vector& VisibleTileScreenSpaceErrors) { +// forEachRenderableVoxelTile( +// VisibleTiles, +// [&VisibleTileScreenSpaceErrors, +// &priorityQueue = this->_visibleTileQueue, +// &octree = this->_octree]( +// size_t index, +// const UCesiumGltfVoxelComponent* pVoxel) { +// double sse = VisibleTileScreenSpaceErrors[index]; +// FVoxelOctree::Node* pNode = octree.getNode(pVoxel->TileId); +// if (pNode) { +// pNode->lastKnownScreenSpaceError = sse; +// } +// +// // Don't create the missing node just yet? It may not be added to the +// // tree depending on the priority of other nodes. +// priorityQueue.push({pVoxel, sse, computePriority(sse)}); +// }); +// +// if (this->_visibleTileQueue.empty()) { +// return; +// } +// +// // Sort the existing nodes in the megatexture by highest to lowest priority. +// std::sort( +// this->_loadedNodeIds.begin(), +// this->_loadedNodeIds.end(), +// [&octree = this->_octree]( +// const CesiumGeometry::OctreeTileID& lhs, +// const CesiumGeometry::OctreeTileID& rhs) { +// const FVoxelOctree::Node* pLeft = octree.getNode(lhs); +// const FVoxelOctree::Node* pRight = octree.getNode(rhs); +// if (!pLeft) { +// return false; +// } +// if (!pRight) { +// return true; +// } +// return computePriority(pLeft->lastKnownScreenSpaceError) > +// computePriority(pRight->lastKnownScreenSpaceError); +// }); +// +// bool shouldUpdateOctree = false; +// // It is possible for the data textures to not exist (e.g., the default voxel +// // material), so check this explicitly. +// bool dataTexturesExist = this->_dataTextures.getTextureCount() > 0; +// +// size_t existingNodeCount = this->_loadedNodeIds.size(); +// size_t destroyedNodeCount = 0; +// size_t addedNodeCount = 0; +// +// if (dataTexturesExist) { +// // For all of the visible nodes... +// for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { +// const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); +// const CesiumGeometry::OctreeTileID& currentTileId = +// currentTile.pComponent->TileId; +// FVoxelOctree::Node* pNode = this->_octree.getNode(currentTileId); +// if (pNode && pNode->dataIndex >= 0) { +// // Node has already been loaded into the data textures. +// pNode->isDataReady = this->_dataTextures.isSlotLoaded(pNode->dataIndex); +// continue; +// } +// +// // Otherwise, check that the data textures have the space to add it. +// const UCesiumGltfVoxelComponent* pVoxel = currentTile.pComponent; +// size_t addNodeIndex = 0; +// if (this->_dataTextures.isFull()) { +// addNodeIndex = existingNodeCount - 1 - destroyedNodeCount; +// if (addNodeIndex >= this->_loadedNodeIds.size()) { +// // This happens when all of the previously loaded nodes have been +// // replaced with new ones. +// continue; +// } +// +// destroyedNodeCount++; +// +// const CesiumGeometry::OctreeTileID& lowestPriorityId = +// this->_loadedNodeIds[addNodeIndex]; +// FVoxelOctree::Node* pLowestPriorityNode = +// this->_octree.getNode(lowestPriorityId); +// +// // Release the data slot of the lowest priority node. +// this->_dataTextures.release(pLowestPriorityNode->dataIndex); +// pLowestPriorityNode->dataIndex = -1; +// pLowestPriorityNode->isDataReady = false; +// +// // Attempt to remove the node and simplify the octree. +// // Will not succeed if the node's siblings are renderable, or if this +// // node contains renderable children. +// shouldUpdateOctree |= this->_octree.removeNode(lowestPriorityId); +// } else { +// addNodeIndex = existingNodeCount + addedNodeCount; +// addedNodeCount++; +// } +// +// // Create the node if it does not already exist in the tree. +// bool createdNewNode = this->_octree.createNode(currentTileId); +// pNode = this->_octree.getNode(currentTileId); +// pNode->lastKnownScreenSpaceError = currentTile.sse; +// +// pNode->dataIndex = this->_dataTextures.add(*pVoxel); +// bool addedToDataTexture = (pNode->dataIndex >= 0); +// shouldUpdateOctree |= createdNewNode || addedToDataTexture; +// +// if (!addedToDataTexture) { +// continue; +// } else if (addNodeIndex < this->_loadedNodeIds.size()) { +// this->_loadedNodeIds[addNodeIndex] = currentTileId; +// } else { +// this->_loadedNodeIds.push_back(currentTileId); +// } +// } +// } else { +// // If there are no data textures, then for all of the visible nodes... +// for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { +// const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); +// const CesiumGeometry::OctreeTileID& currentTileId = +// currentTile.pComponent->TileId; +// // Create the node if it does not already exist in the tree. +// shouldUpdateOctree |= this->_octree.createNode(currentTileId); +// +// FVoxelOctree::Node* pNode = this->_octree.getNode(currentTileId); +// pNode->lastKnownScreenSpaceError = currentTile.sse; +// // Set to arbitrary index. This will prompt the tile to render even though +// // it does not actually have data. +// pNode->dataSlotIndex = 0; +// } +// } +// +// if (shouldUpdateOctree) { +// this->_octree.updateTexture(); +// } +//} +// +//double FVoxelResources::computePriority(double sse) { +// return 10.0 * sse / (sse + 1.0); +//} diff --git a/Source/CesiumRuntime/Private/VoxelResources.h b/Source/CesiumRuntime/Private/VoxelResources.h index 36a04ffab..6aff08f7a 100644 --- a/Source/CesiumRuntime/Private/VoxelResources.h +++ b/Source/CesiumRuntime/Private/VoxelResources.h @@ -1,119 +1,119 @@ -// Copyright 2020-2024 CesiumGS, Inc. and Contributors - -#pragma once - -#include "CesiumCommon.h" -#include "CesiumGltfVoxelComponent.h" -#include "Engine/VolumeTexture.h" -#include "VoxelDataTextures.h" -#include "VoxelOctree.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace Cesium3DTilesSelection { -class Tile; -} - -enum class EVoxelGridShape : uint8; -struct FCesiumVoxelClassDescription; - -class FVoxelResources { -public: - /** - * Value constants taken from CesiumJS. - */ - static const uint32 MaximumOctreeTextureWidth = 2048; - static const uint32 MaximumDataTextureMemoryBytes = 512 * 1024 * 1024; - static const uint32 DefaultDataTextureMemoryBytes = 128 * 1024 * 1024; - - /** - * @brief Constructs the resources necessary to render voxel data in an Unreal - * material. - * - * @param pVoxelClass The voxel class description, indicating which metadata - * attributes to encode. - * @param shape The shape of the voxel grid, which affects how voxel data is - * read and stored. - * @param dataDimensions The dimensions of the voxel data in each tile, - * including padding. - * @param featureLevel The RHI feature level associated with the scene. - * @param requestedMemoryPerDataTexture The requested texture memory for the - * data texture constructed for each voxel attribute. - */ - FVoxelResources( - const FCesiumVoxelClassDescription* pVoxelClass, - EVoxelGridShape shape, - const glm::uvec3& dataDimensions, - ERHIFeatureLevel::Type featureLevel, - uint32 requestedMemoryPerDataTexture = DefaultDataTextureMemoryBytes); - ~FVoxelResources(); - - /** - * @brief Retrieves how many tiles there in the megatexture along each - * dimension. - */ - FVector GetTileCount() const; - - /** - * @brief Retrieves the texture containing the encoded octree. - */ - UTexture2D* GetOctreeTexture() const { return this->_octree.getTexture(); } - - /** - * @brief Retrieves the texture containing the data for the attribute with - * the given ID. Returns nullptr if the attribute does not exist. - */ - UTexture* GetDataTexture(const FString& attributeId) const { - return this->_dataTextures.getTexture(attributeId); - } - - /** - * Updates the resources given the currently visible tiles. - */ - void Update( - const std::vector& VisibleTiles, - const std::vector& VisibleTileScreenSpaceErrors); - -private: - static double computePriority(double sse); - - struct VoxelTileUpdateInfo { - const UCesiumGltfVoxelComponent* pComponent; - double sse; - double priority; - }; - - struct ScreenSpaceErrorGreaterComparator { - bool - operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { - return lhs.priority > rhs.priority; - } - }; - - struct ScreenSpaceErrorLessComparator { - bool - operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { - return lhs.priority < rhs.priority; - } - }; - - FVoxelOctree _octree; - FVoxelDataTextures _dataTextures; - - using MaxPriorityQueue = std::priority_queue< - VoxelTileUpdateInfo, - std::vector, - ScreenSpaceErrorLessComparator>; - - std::vector _loadedNodeIds; - MaxPriorityQueue _visibleTileQueue; -}; +//// Copyright 2020-2024 CesiumGS, Inc. and Contributors +// +//#pragma once +// +//#include "CesiumCommon.h" +//#include "CesiumGltfVoxelComponent.h" +//#include "Engine/VolumeTexture.h" +//#include "VoxelDataTextures.h" +//#include "VoxelOctree.h" +// +//#include +//#include +//#include +//#include +// +//#include +//#include +//#include +//#include +//#include +// +//namespace Cesium3DTilesSelection { +//class Tile; +//} +// +//enum class EVoxelGridShape : uint8; +//struct FCesiumVoxelClassDescription; +// +//class FVoxelResources { +//public: +// /** +// * Value constants taken from CesiumJS. +// */ +// static const uint32 MaximumOctreeTextureWidth = 2048; +// static const uint32 MaximumDataTextureMemoryBytes = 512 * 1024 * 1024; +// static const uint32 DefaultDataTextureMemoryBytes = 128 * 1024 * 1024; +// +// /** +// * @brief Constructs the resources necessary to render voxel data in an Unreal +// * material. +// * +// * @param pVoxelClass The voxel class description, indicating which metadata +// * attributes to encode. +// * @param shape The shape of the voxel grid, which affects how voxel data is +// * read and stored. +// * @param dataDimensions The dimensions of the voxel data in each tile, +// * including padding. +// * @param featureLevel The RHI feature level associated with the scene. +// * @param requestedMemoryPerDataTexture The requested texture memory for the +// * data texture constructed for each voxel attribute. +// */ +// FVoxelResources( +// const FCesiumVoxelClassDescription* pVoxelClass, +// EVoxelGridShape shape, +// const glm::uvec3& dataDimensions, +// ERHIFeatureLevel::Type featureLevel, +// uint32 requestedMemoryPerDataTexture = DefaultDataTextureMemoryBytes); +// ~FVoxelResources(); +// +// /** +// * @brief Retrieves how many tiles there in the megatexture along each +// * dimension. +// */ +// FVector GetTileCount() const; +// +// /** +// * @brief Retrieves the texture containing the encoded octree. +// */ +// UTexture2D* GetOctreeTexture() const { return this->_octree.getTexture(); } +// +// /** +// * @brief Retrieves the texture containing the data for the attribute with +// * the given ID. Returns nullptr if the attribute does not exist. +// */ +// UTexture* GetDataTexture(const FString& attributeId) const { +// return this->_dataTextures.getTexture(attributeId); +// } +// +// /** +// * Updates the resources given the currently visible tiles. +// */ +// void Update( +// const std::vector& VisibleTiles, +// const std::vector& VisibleTileScreenSpaceErrors); +// +//private: +// static double computePriority(double sse); +// +// struct VoxelTileUpdateInfo { +// const UCesiumGltfVoxelComponent* pComponent; +// double sse; +// double priority; +// }; +// +// struct ScreenSpaceErrorGreaterComparator { +// bool +// operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { +// return lhs.priority > rhs.priority; +// } +// }; +// +// struct ScreenSpaceErrorLessComparator { +// bool +// operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { +// return lhs.priority < rhs.priority; +// } +// }; +// +// FVoxelOctree _octree; +// UVoxelDataTextures _dataTextures; +// +// using MaxPriorityQueue = std::priority_queue< +// VoxelTileUpdateInfo, +// std::vector, +// ScreenSpaceErrorLessComparator>; +// +// std::vector _loadedNodeIds; +// MaxPriorityQueue _visibleTileQueue; +//}; diff --git a/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h index 5b51ac847..800c2b5b3 100644 --- a/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h +++ b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h @@ -151,7 +151,7 @@ class CESIUMRUNTIME_API UCesiumVoxelMetadataComponent : public UActorComponent { private: #if WITH_EDITOR - UVolumeTexture* DefaultVolumeTexture; + TObjectPtr pDefaultVolumeTexture; static const FString ShaderPreviewTemplate; void UpdateShaderPreview(); From d41d68c6f138b5bec6be1c6b08d21bd228475d6f Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 17 Jun 2025 15:53:04 -0400 Subject: [PATCH 15/34] Delete files --- .../CesiumRuntime/Private/VoxelResources.cpp | 227 ------------------ Source/CesiumRuntime/Private/VoxelResources.h | 119 --------- 2 files changed, 346 deletions(-) delete mode 100644 Source/CesiumRuntime/Private/VoxelResources.cpp delete mode 100644 Source/CesiumRuntime/Private/VoxelResources.h diff --git a/Source/CesiumRuntime/Private/VoxelResources.cpp b/Source/CesiumRuntime/Private/VoxelResources.cpp deleted file mode 100644 index a812dba4f..000000000 --- a/Source/CesiumRuntime/Private/VoxelResources.cpp +++ /dev/null @@ -1,227 +0,0 @@ -//// Copyright 2020-2024 CesiumGS, Inc. and Contributors -// -//#include "VoxelResources.h" -//#include "CesiumGltfComponent.h" -//#include "CesiumRuntime.h" -//#include "EncodedMetadataConversions.h" -//#include "VoxelGridShape.h" -// -//#include -//#include -//#include -//#include -//#include -// -//#include -//#include -// -//using namespace CesiumGltf; -//using namespace Cesium3DTilesContent; -// -//FVoxelResources::FVoxelResources( -// const FCesiumVoxelClassDescription* pVoxelClass, -// EVoxelGridShape shape, -// const glm::uvec3& dataDimensions, -// ERHIFeatureLevel::Type featureLevel, -// uint32 requestedMemoryPerDataTexture) -// : _dataTextures( -// pVoxelClass, -// dataDimensions, -// featureLevel, -// requestedMemoryPerDataTexture), -// _loadedNodeIds(), -// _visibleTileQueue() { -// uint32 width = MaximumOctreeTextureWidth; -// uint32 maximumTileCount = this->_dataTextures.getMaximumTileCount(); -// this->_octree.initializeTexture(width, maximumTileCount); -// this->_loadedNodeIds.reserve(maximumTileCount); -//} -// -//FVoxelResources::~FVoxelResources() {} -// -//FVector FVoxelResources::GetTileCount() const { -// auto tileCount = this->_dataTextures.getTileCountAlongAxes(); -// return FVector(tileCount.x, tileCount.y, tileCount.z); -//} -// -//namespace { -//template -//void forEachRenderableVoxelTile(const auto& tiles, Func&& f) { -// for (size_t i = 0; i < tiles.size(); i++) { -// const Cesium3DTilesSelection::Tile::Pointer& pTile = tiles[i]; -// if (!pTile || -// pTile->getState() != Cesium3DTilesSelection::TileLoadState::Done) { -// continue; -// } -// -// const Cesium3DTilesSelection::TileContent& content = pTile->getContent(); -// const Cesium3DTilesSelection::TileRenderContent* pRenderContent = -// content.getRenderContent(); -// if (!pRenderContent) { -// continue; -// } -// -// UCesiumGltfComponent* Gltf = static_cast( -// pRenderContent->getRenderResources()); -// if (!Gltf) { -// // When a tile does not have render resources (i.e. a glTF), then -// // the resources either have not yet been loaded or prepared, -// // or the tile is from an external tileset and does not directly -// // own renderable content. In both cases, the tile is ignored here. -// continue; -// } -// -// const TArray& Children = Gltf->GetAttachChildren(); -// for (USceneComponent* pChild : Children) { -// UCesiumGltfVoxelComponent* pVoxelComponent = -// Cast(pChild); -// if (!pVoxelComponent) { -// continue; -// } -// -// f(i, pVoxelComponent); -// } -// } -//} -//} // namespace -// -//void FVoxelResources::Update( -// const std::vector& VisibleTiles, -// const std::vector& VisibleTileScreenSpaceErrors) { -// forEachRenderableVoxelTile( -// VisibleTiles, -// [&VisibleTileScreenSpaceErrors, -// &priorityQueue = this->_visibleTileQueue, -// &octree = this->_octree]( -// size_t index, -// const UCesiumGltfVoxelComponent* pVoxel) { -// double sse = VisibleTileScreenSpaceErrors[index]; -// FVoxelOctree::Node* pNode = octree.getNode(pVoxel->TileId); -// if (pNode) { -// pNode->lastKnownScreenSpaceError = sse; -// } -// -// // Don't create the missing node just yet? It may not be added to the -// // tree depending on the priority of other nodes. -// priorityQueue.push({pVoxel, sse, computePriority(sse)}); -// }); -// -// if (this->_visibleTileQueue.empty()) { -// return; -// } -// -// // Sort the existing nodes in the megatexture by highest to lowest priority. -// std::sort( -// this->_loadedNodeIds.begin(), -// this->_loadedNodeIds.end(), -// [&octree = this->_octree]( -// const CesiumGeometry::OctreeTileID& lhs, -// const CesiumGeometry::OctreeTileID& rhs) { -// const FVoxelOctree::Node* pLeft = octree.getNode(lhs); -// const FVoxelOctree::Node* pRight = octree.getNode(rhs); -// if (!pLeft) { -// return false; -// } -// if (!pRight) { -// return true; -// } -// return computePriority(pLeft->lastKnownScreenSpaceError) > -// computePriority(pRight->lastKnownScreenSpaceError); -// }); -// -// bool shouldUpdateOctree = false; -// // It is possible for the data textures to not exist (e.g., the default voxel -// // material), so check this explicitly. -// bool dataTexturesExist = this->_dataTextures.getTextureCount() > 0; -// -// size_t existingNodeCount = this->_loadedNodeIds.size(); -// size_t destroyedNodeCount = 0; -// size_t addedNodeCount = 0; -// -// if (dataTexturesExist) { -// // For all of the visible nodes... -// for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { -// const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); -// const CesiumGeometry::OctreeTileID& currentTileId = -// currentTile.pComponent->TileId; -// FVoxelOctree::Node* pNode = this->_octree.getNode(currentTileId); -// if (pNode && pNode->dataIndex >= 0) { -// // Node has already been loaded into the data textures. -// pNode->isDataReady = this->_dataTextures.isSlotLoaded(pNode->dataIndex); -// continue; -// } -// -// // Otherwise, check that the data textures have the space to add it. -// const UCesiumGltfVoxelComponent* pVoxel = currentTile.pComponent; -// size_t addNodeIndex = 0; -// if (this->_dataTextures.isFull()) { -// addNodeIndex = existingNodeCount - 1 - destroyedNodeCount; -// if (addNodeIndex >= this->_loadedNodeIds.size()) { -// // This happens when all of the previously loaded nodes have been -// // replaced with new ones. -// continue; -// } -// -// destroyedNodeCount++; -// -// const CesiumGeometry::OctreeTileID& lowestPriorityId = -// this->_loadedNodeIds[addNodeIndex]; -// FVoxelOctree::Node* pLowestPriorityNode = -// this->_octree.getNode(lowestPriorityId); -// -// // Release the data slot of the lowest priority node. -// this->_dataTextures.release(pLowestPriorityNode->dataIndex); -// pLowestPriorityNode->dataIndex = -1; -// pLowestPriorityNode->isDataReady = false; -// -// // Attempt to remove the node and simplify the octree. -// // Will not succeed if the node's siblings are renderable, or if this -// // node contains renderable children. -// shouldUpdateOctree |= this->_octree.removeNode(lowestPriorityId); -// } else { -// addNodeIndex = existingNodeCount + addedNodeCount; -// addedNodeCount++; -// } -// -// // Create the node if it does not already exist in the tree. -// bool createdNewNode = this->_octree.createNode(currentTileId); -// pNode = this->_octree.getNode(currentTileId); -// pNode->lastKnownScreenSpaceError = currentTile.sse; -// -// pNode->dataIndex = this->_dataTextures.add(*pVoxel); -// bool addedToDataTexture = (pNode->dataIndex >= 0); -// shouldUpdateOctree |= createdNewNode || addedToDataTexture; -// -// if (!addedToDataTexture) { -// continue; -// } else if (addNodeIndex < this->_loadedNodeIds.size()) { -// this->_loadedNodeIds[addNodeIndex] = currentTileId; -// } else { -// this->_loadedNodeIds.push_back(currentTileId); -// } -// } -// } else { -// // If there are no data textures, then for all of the visible nodes... -// for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { -// const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); -// const CesiumGeometry::OctreeTileID& currentTileId = -// currentTile.pComponent->TileId; -// // Create the node if it does not already exist in the tree. -// shouldUpdateOctree |= this->_octree.createNode(currentTileId); -// -// FVoxelOctree::Node* pNode = this->_octree.getNode(currentTileId); -// pNode->lastKnownScreenSpaceError = currentTile.sse; -// // Set to arbitrary index. This will prompt the tile to render even though -// // it does not actually have data. -// pNode->dataSlotIndex = 0; -// } -// } -// -// if (shouldUpdateOctree) { -// this->_octree.updateTexture(); -// } -//} -// -//double FVoxelResources::computePriority(double sse) { -// return 10.0 * sse / (sse + 1.0); -//} diff --git a/Source/CesiumRuntime/Private/VoxelResources.h b/Source/CesiumRuntime/Private/VoxelResources.h deleted file mode 100644 index 6aff08f7a..000000000 --- a/Source/CesiumRuntime/Private/VoxelResources.h +++ /dev/null @@ -1,119 +0,0 @@ -//// Copyright 2020-2024 CesiumGS, Inc. and Contributors -// -//#pragma once -// -//#include "CesiumCommon.h" -//#include "CesiumGltfVoxelComponent.h" -//#include "Engine/VolumeTexture.h" -//#include "VoxelDataTextures.h" -//#include "VoxelOctree.h" -// -//#include -//#include -//#include -//#include -// -//#include -//#include -//#include -//#include -//#include -// -//namespace Cesium3DTilesSelection { -//class Tile; -//} -// -//enum class EVoxelGridShape : uint8; -//struct FCesiumVoxelClassDescription; -// -//class FVoxelResources { -//public: -// /** -// * Value constants taken from CesiumJS. -// */ -// static const uint32 MaximumOctreeTextureWidth = 2048; -// static const uint32 MaximumDataTextureMemoryBytes = 512 * 1024 * 1024; -// static const uint32 DefaultDataTextureMemoryBytes = 128 * 1024 * 1024; -// -// /** -// * @brief Constructs the resources necessary to render voxel data in an Unreal -// * material. -// * -// * @param pVoxelClass The voxel class description, indicating which metadata -// * attributes to encode. -// * @param shape The shape of the voxel grid, which affects how voxel data is -// * read and stored. -// * @param dataDimensions The dimensions of the voxel data in each tile, -// * including padding. -// * @param featureLevel The RHI feature level associated with the scene. -// * @param requestedMemoryPerDataTexture The requested texture memory for the -// * data texture constructed for each voxel attribute. -// */ -// FVoxelResources( -// const FCesiumVoxelClassDescription* pVoxelClass, -// EVoxelGridShape shape, -// const glm::uvec3& dataDimensions, -// ERHIFeatureLevel::Type featureLevel, -// uint32 requestedMemoryPerDataTexture = DefaultDataTextureMemoryBytes); -// ~FVoxelResources(); -// -// /** -// * @brief Retrieves how many tiles there in the megatexture along each -// * dimension. -// */ -// FVector GetTileCount() const; -// -// /** -// * @brief Retrieves the texture containing the encoded octree. -// */ -// UTexture2D* GetOctreeTexture() const { return this->_octree.getTexture(); } -// -// /** -// * @brief Retrieves the texture containing the data for the attribute with -// * the given ID. Returns nullptr if the attribute does not exist. -// */ -// UTexture* GetDataTexture(const FString& attributeId) const { -// return this->_dataTextures.getTexture(attributeId); -// } -// -// /** -// * Updates the resources given the currently visible tiles. -// */ -// void Update( -// const std::vector& VisibleTiles, -// const std::vector& VisibleTileScreenSpaceErrors); -// -//private: -// static double computePriority(double sse); -// -// struct VoxelTileUpdateInfo { -// const UCesiumGltfVoxelComponent* pComponent; -// double sse; -// double priority; -// }; -// -// struct ScreenSpaceErrorGreaterComparator { -// bool -// operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { -// return lhs.priority > rhs.priority; -// } -// }; -// -// struct ScreenSpaceErrorLessComparator { -// bool -// operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { -// return lhs.priority < rhs.priority; -// } -// }; -// -// FVoxelOctree _octree; -// UVoxelDataTextures _dataTextures; -// -// using MaxPriorityQueue = std::priority_queue< -// VoxelTileUpdateInfo, -// std::vector, -// ScreenSpaceErrorLessComparator>; -// -// std::vector _loadedNodeIds; -// MaxPriorityQueue _visibleTileQueue; -//}; From 6f9637c3b3c67333283b88a22f64cd3d07b17bed Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Tue, 17 Jun 2025 17:38:03 -0400 Subject: [PATCH 16/34] Remove custom texture resource implementations --- .../Private/CesiumTextureResource.cpp | 112 ++++++++++++++++++ .../Private/CesiumTextureResource.h | 30 +++++ .../Private/CesiumVoxelRendererComponent.cpp | 25 ++-- .../Private/CesiumVoxelRendererComponent.h | 2 + .../Private/VoxelDataTextures.cpp | 77 +----------- .../CesiumRuntime/Private/VoxelDataTextures.h | 3 + Source/CesiumRuntime/Private/VoxelOctree.cpp | 89 ++------------ Source/CesiumRuntime/Private/VoxelOctree.h | 7 +- 8 files changed, 184 insertions(+), 161 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp index 5ffed9684..77a1c01d3 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp @@ -98,6 +98,28 @@ class FCesiumCreateNewTextureResource : public FCesiumTextureResource { std::vector _pixelData; }; +/** + * A Cesium texture resource that creates an empty `FRHITexture` when `InitRHI` + * is called from the render thread. + */ +class FCesiumCreateEmptyTextureResource : public FCesiumTextureResource { +public: + FCesiumCreateEmptyTextureResource( + TextureGroup textureGroup, + uint32 width, + uint32 height, + uint32 depth, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + uint32 extData); + +protected: + virtual FTextureRHIRef InitializeTextureRHI() override; +}; + ESamplerFilter convertFilter(TextureFilter filter) { switch (filter) { case TF_Nearest: @@ -436,6 +458,29 @@ FCesiumTextureResourceUniquePtr FCesiumTextureResource::CreateWrapped( false)); } +FCesiumTextureResourceUniquePtr FCesiumTextureResource::CreateEmpty( + TextureGroup textureGroup, + uint32 width, + uint32 height, + uint32 depth, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB) { + return FCesiumTextureResourceUniquePtr(new FCesiumCreateEmptyTextureResource( + textureGroup, + width, + height, + depth, + format, + filter, + addressX, + addressY, + sRGB, + 0)); +} + /*static*/ void FCesiumTextureResource::Destroy(FCesiumTextureResource* p) { if (p == nullptr) return; @@ -773,3 +818,70 @@ FTextureRHIRef FCesiumCreateNewTextureResource::InitializeTextureRHI() { return rhiTexture; } + +FCesiumCreateEmptyTextureResource::FCesiumCreateEmptyTextureResource( + TextureGroup textureGroup, + uint32 width, + uint32 height, + uint32 depth, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + uint32 extData) + : FCesiumTextureResource( + textureGroup, + width, + height, + depth, + format, + filter, + addressX, + addressY, + sRGB, + false, + extData, + true) {} + +FTextureRHIRef FCesiumCreateEmptyTextureResource::InitializeTextureRHI() { + FString debugName = TEXT("CesiumTextureUtility"); + + FRHIResourceCreateInfo createInfo{*debugName}; + createInfo.BulkData = nullptr; + createInfo.ExtData = this->_platformExtData; + + ETextureCreateFlags textureFlags = TexCreate_ShaderResource; + if (this->bSRGB) { + textureFlags |= TexCreate_SRGB; + } + + if (this->_depth > 1) { + // Create a new empty RHI texture (3D). + return RHICreateTexture( + FRHITextureCreateDesc::Create3D(createInfo.DebugName) + .SetExtent(int32(this->_width), int32(this->_height)) + .SetDepth(this->_depth) + .SetFormat(this->_format) + .SetNumMips(1) + .SetNumSamples(1) + .SetFlags(textureFlags) + .SetInitialState(ERHIAccess::Unknown) + .SetExtData(createInfo.ExtData) + .SetGPUMask(createInfo.GPUMask) + .SetClearValue(createInfo.ClearValueBinding)); + } + + // Create a new empty RHI texture (2D). + return RHICreateTexture( + FRHITextureCreateDesc::Create2D(createInfo.DebugName) + .SetExtent(int32(this->_width), int32(this->_height)) + .SetFormat(this->_format) + .SetNumMips(1) + .SetNumSamples(1) + .SetFlags(textureFlags) + .SetInitialState(ERHIAccess::Unknown) + .SetExtData(createInfo.ExtData) + .SetGPUMask(createInfo.GPUMask) + .SetClearValue(createInfo.ClearValueBinding)); +} diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.h b/Source/CesiumRuntime/Private/CesiumTextureResource.h index f815166bb..95543805e 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.h +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.h @@ -71,6 +71,36 @@ class FCesiumTextureResource : public FTextureResource { bool sRGB, bool useMipMapsIfAvailable); + /** + * Create a new empty FCesiumTextureResource with the given parameters. Useful + * for textures that are procedurally generated or updated at runtime. + * + * @param textureGroup The texture group in which to create this texture. + * @param width The width of the texture. + * @param height The height of the texture. + * @param depth The depth of the texture. + * @param format The pixel format of the texture. + * @param filter The texture filtering to use when sampling this texture. + * @param addressX The X texture addressing mode to use when sampling this + * texture. + * @param addressY The Y texture addressing mode to use when sampling this + * texture. + * @param sRGB True if the image data stored in this texture should be treated + * as sRGB. + * @return The created texture resource, or nullptr if a texture could not be + * created. + */ + static FCesiumTextureResourceUniquePtr CreateEmpty( + TextureGroup textureGroup, + uint32 width, + uint32 height, + uint32 depth, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB); + /** * Destroys an FCesiumTextureResource. Unreal TextureResources must be * destroyed on the render thread, so it is important not to call `delete` diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 99509c8ff..a4c7a5624 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -60,11 +60,17 @@ void UCesiumVoxelRendererComponent::BeginDestroy() { // Reset the pointers. this->MeshComponent = nullptr; this->_pOctree.Reset(); -// this->_pDataTextures->BeginDestroy(); + // this->_pDataTextures->BeginDestroy(); Super::BeginDestroy(); } +bool UCesiumVoxelRendererComponent::IsReadyForFinishDestroy() { + if (this->_pDataTextures.IsValid()) { + } + return Super::IsReadyForFinishDestroy(); +} + namespace { EVoxelGridShape getVoxelGridShape( const Cesium3DTilesSelection::BoundingVolume& boundingVolume) { @@ -92,8 +98,8 @@ void setVoxelBoxProperties( // 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. + // explicit cube mesh. In other words, this scale must be applied in-shader + // to account for the actual mesh's bounds. pVoxelMaterial->SetVectorParameterValueByInfo( FMaterialParameterInfo( UTF8_TO_TCHAR("Shape TransformToUnit Row 0"), @@ -125,7 +131,8 @@ getMetadataValue(const std::optional& jsonValue) { return FCesiumMetadataValue(); } - // Attempt to convert the array to a vec4 (or a value with less dimensions). + // Attempt to convert the array to a vec4 (or a value with less + // dimensions). size_t endIndex = FMath::Min(array.size(), (size_t)4); TArray values; for (size_t i = 0; i < endIndex; i++) { @@ -464,7 +471,8 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( // const Cesium3DTiles::MetadataEntity& metadata = // *tilesetMetadata.metadata; // // TODO: This should find the property by "TILESET_TILE_COUNT" - // if (metadata.properties.find("tileCount") != metadata.properties.end()) { + // if (metadata.properties.find("tileCount") != metadata.properties.end()) + // { // const CesiumUtility::JsonValue& value = // metadata.properties.at("tileCount"); // if (value.isInt64()) { @@ -477,7 +485,8 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( // if (knownTileCount > 0) { // uint32 maximumTextureMemory = - // getMaximumTextureMemory(pDescription, dataDimensions, knownTileCount); + // getMaximumTextureMemory(pDescription, dataDimensions, + // knownTileCount); // requestedTextureMemory = FMath::Min( // maximumTextureMemory, // FVoxelResources::MaximumDataTextureMemoryBytes); @@ -690,8 +699,8 @@ void UCesiumVoxelRendererComponent::UpdateTiles( FVoxelOctree::Node* pNode = this->_pOctree->getNode(currentTileId); pNode->lastKnownScreenSpaceError = currentTile.sse; - // Set to arbitrary index. This will prompt the tile to render even though - // it does not actually have data. + // Set to arbitrary index. This will prompt the tile to render even + // though it does not actually have data. pNode->dataIndex = 0; pNode->isDataReady = true; } diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h index 018077058..f852bf369 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h @@ -52,6 +52,8 @@ class UCesiumVoxelRendererComponent : public USceneComponent { virtual ~UCesiumVoxelRendererComponent(); void BeginDestroy() override; + bool IsReadyForFinishDestroy() override; + UPROPERTY(EditAnywhere, Category = "Cesium") UMaterialInterface* DefaultMaterial = nullptr; diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp index a8d0d6bc9..06ae88e16 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp @@ -20,78 +20,6 @@ using namespace CesiumGltf; using namespace EncodedFeaturesMetadata; -/** - * A Cesium texture resource that creates an initially empty `FRHITexture` for - * FVoxelDataTextures. - */ -class FCesiumVoxelDataTextureResource : public FCesiumTextureResource { -public: - FCesiumVoxelDataTextureResource( - TextureGroup textureGroup, - uint32 width, - uint32 height, - uint32 depth, - EPixelFormat format, - TextureFilter filter, - TextureAddress addressX, - TextureAddress addressY, - bool sRGB, - uint32 extData); - -protected: - virtual FTextureRHIRef InitializeTextureRHI() override; -}; - -FCesiumVoxelDataTextureResource::FCesiumVoxelDataTextureResource( - TextureGroup textureGroup, - uint32 width, - uint32 height, - uint32 depth, - EPixelFormat format, - TextureFilter filter, - TextureAddress addressX, - TextureAddress addressY, - bool sRGB, - uint32 extData) - : FCesiumTextureResource( - textureGroup, - width, - height, - depth, - format, - filter, - addressX, - addressY, - sRGB, - false, - extData, - true) {} - -FTextureRHIRef FCesiumVoxelDataTextureResource::InitializeTextureRHI() { - FRHIResourceCreateInfo createInfo{TEXT("FCesiumVoxelDataTextureResource")}; - createInfo.BulkData = nullptr; - createInfo.ExtData = this->_platformExtData; - - ETextureCreateFlags textureFlags = TexCreate_ShaderResource; - if (this->bSRGB) { - textureFlags |= TexCreate_SRGB; - } - - // Create a new 3D RHI texture, initially empty. - return RHICreateTexture( - FRHITextureCreateDesc::Create3D(createInfo.DebugName) - .SetExtent(int32(this->_width), int32(this->_height)) - .SetDepth(this->_depth) - .SetFormat(this->_format) - .SetNumMips(1) - .SetNumSamples(1) - .SetFlags(textureFlags) - .SetInitialState(ERHIAccess::Unknown) - .SetExtData(createInfo.ExtData) - .SetGPUMask(createInfo.GPUMask) - .SetClearValue(createInfo.ClearValueBinding)); -} - FVoxelDataTextures::FVoxelDataTextures( const FCesiumVoxelClassDescription* pVoxelClass, const glm::uvec3& dataDimensions, @@ -196,7 +124,7 @@ FVoxelDataTextures::FVoxelDataTextures( // Create the actual textures. for (auto& propertyIt : this->_propertyMap) { FCesiumTextureResource* pTextureResource = - MakeUnique( + FCesiumTextureResource::CreateEmpty( TextureGroup::TEXTUREGROUP_8BitData, actualDimensions.x, actualDimensions.y, @@ -205,8 +133,7 @@ FVoxelDataTextures::FVoxelDataTextures( TextureFilter::TF_Nearest, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, - false, - 0) + false) .Release(); UVolumeTexture* pTexture = NewObject( diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.h b/Source/CesiumRuntime/Private/VoxelDataTextures.h index 5573e0f8c..ac59f2806 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.h +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.h @@ -130,6 +130,9 @@ class FVoxelDataTextures { /** * @brief A pointer to the texture resource. There is no way to retrieve * this through the UTexture API, so the pointer is stored here. + * + * Although this would ideally be a TUniquePtr, it prevents TMap from + * compiling. */ FCesiumTextureResource* pResource; }; diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index 442a07bf5..95c84494a 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -10,75 +10,6 @@ using namespace CesiumGeometry; using namespace Cesium3DTilesContent; -/** - * A Cesium texture resource that creates an initially empty `FRHITexture` for - * FVoxelOctree. - */ -class FCesiumVoxelOctreeTextureResource : public FCesiumTextureResource { -public: - FCesiumVoxelOctreeTextureResource( - TextureGroup textureGroup, - uint32 width, - uint32 height, - EPixelFormat format, - TextureFilter filter, - TextureAddress addressX, - TextureAddress addressY, - bool sRGB, - uint32 extData); - -protected: - virtual FTextureRHIRef InitializeTextureRHI() override; -}; - -FCesiumVoxelOctreeTextureResource::FCesiumVoxelOctreeTextureResource( - TextureGroup textureGroup, - uint32 width, - uint32 height, - EPixelFormat format, - TextureFilter filter, - TextureAddress addressX, - TextureAddress addressY, - bool sRGB, - uint32 extData) - : FCesiumTextureResource( - textureGroup, - width, - height, - 0, - format, - filter, - addressX, - addressY, - sRGB, - false, - extData, - true) {} - -FTextureRHIRef FCesiumVoxelOctreeTextureResource::InitializeTextureRHI() { - FRHIResourceCreateInfo createInfo{TEXT("FVoxelOctreeTextureResource")}; - createInfo.BulkData = nullptr; - createInfo.ExtData = this->_platformExtData; - - ETextureCreateFlags textureFlags = TexCreate_ShaderResource; - if (this->bSRGB) { - textureFlags |= TexCreate_SRGB; - } - - // Create a new 2D RHI texture, initially empty. - return RHICreateTexture( - FRHITextureCreateDesc::Create2D(createInfo.DebugName) - .SetExtent(int32(this->_width), int32(this->_height)) - .SetFormat(this->_format) - .SetNumMips(1) - .SetNumSamples(1) - .SetFlags(textureFlags) - .SetInitialState(ERHIAccess::Unknown) - .SetExtData(createInfo.ExtData) - .SetGPUMask(createInfo.GPUMask) - .SetClearValue(createInfo.ClearValueBinding)); -} - void UCesiumVoxelOctreeTexture::update(const std::vector& data) { if (!this->_pResource || !this->_pResource->TextureRHI) { return; @@ -112,7 +43,7 @@ void UCesiumVoxelOctreeTexture::update(const std::vector& data) { // Pitch = size in bytes of each row of the source image uint32 sourcePitch = region.Width * sizeof(uint32); ENQUEUE_RENDER_COMMAND(Cesium_UpdateResource) - ([pResource = this->_pResource, &data, region, sourcePitch]( + ([pResource = this->_pResource.Get(), &data, region, sourcePitch]( FRHICommandListImmediate& RHICmdList) { RHIUpdateTexture2D( pResource->TextureRHI, @@ -121,6 +52,8 @@ void UCesiumVoxelOctreeTexture::update(const std::vector& data) { sourcePitch, reinterpret_cast(data.data())); }); + + // this->_fence.BeginFence(); } /*static*/ UCesiumVoxelOctreeTexture* @@ -130,17 +63,17 @@ UCesiumVoxelOctreeTexture::create(uint32 Width, uint32 MaximumTileCount) { Height = static_cast(FMath::CeilToInt64(Height)); Height = FMath::Clamp(Height, 1, Width); - TUniquePtr pResource = - MakeUnique( + FCesiumTextureResourceUniquePtr pResource = + FCesiumTextureResource::CreateEmpty( TextureGroup::TEXTUREGROUP_8BitData, Width, Height, + 1, // Depth EPixelFormat::PF_R8G8B8A8, TextureFilter::TF_Nearest, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, - false, - 0); + false); UCesiumVoxelOctreeTexture* pTexture = NewObject( GetTransientPackage(), @@ -158,12 +91,16 @@ UCesiumVoxelOctreeTexture::create(uint32 Width, uint32 MaximumTileCount) { pTexture->NeverStream = true; pTexture->_tilesPerRow = TilesPerRow; - pTexture->_pResource = pResource.Release(); - pTexture->SetResource(pTexture->_pResource); + pTexture->_pResource = std::move(pResource); + pTexture->SetResource(pTexture->_pResource.Get()); return pTexture; } +bool UCesiumVoxelOctreeTexture::isReadyToDestroy() const { + return this->_fence.IsFenceComplete(); +} + size_t FVoxelOctree::OctreeTileIDHash::operator()( const CesiumGeometry::OctreeTileID& tileId) const { // Tiles with the same morton index, but on different levels, are diff --git a/Source/CesiumRuntime/Private/VoxelOctree.h b/Source/CesiumRuntime/Private/VoxelOctree.h index de501ff47..d1e132964 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.h +++ b/Source/CesiumRuntime/Private/VoxelOctree.h @@ -4,6 +4,7 @@ #include "CesiumTextureResource.h" #include "Engine/Texture2D.h" +#include "RenderCommandFence.h" #include #include @@ -31,9 +32,12 @@ class UCesiumVoxelOctreeTexture : public UTexture2D { uint32_t getTilesPerRow() const { return this->_tilesPerRow; } + bool isReadyToDestroy() const; + private: - FCesiumTextureResource* _pResource; + FCesiumTextureResourceUniquePtr _pResource; uint32_t _tilesPerRow; + FRenderCommandFence _fence; }; /** @@ -227,7 +231,6 @@ class FVoxelOctree { uint32 parentOctreeIndex, uint32 parentTextureIndex); - struct OctreeTileIDHash { size_t operator()(const CesiumGeometry::OctreeTileID& tileId) const; }; From bf116465e87067fe9d584ac384bd19f7fd4788e9 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 18 Jun 2025 17:21:17 -0400 Subject: [PATCH 17/34] Add fence to VoxelOctree, other edits --- .../Private/CesiumVoxelRendererComponent.cpp | 4 +- .../Private/CesiumVoxelRendererComponent.h | 1 - Source/CesiumRuntime/Private/VoxelOctree.cpp | 122 +++++------ Source/CesiumRuntime/Private/VoxelOctree.h | 195 ++++++++++-------- 4 files changed, 164 insertions(+), 158 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index a4c7a5624..b37292299 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -492,7 +492,6 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( // FVoxelResources::MaximumDataTextureMemoryBytes); //} - pVoxelComponent->_pOctree = MakeUnique(); if (pDescription && pVoxelMesh->GetScene()) { pVoxelComponent->_pDataTextures = MakeUnique( pDescription, @@ -501,12 +500,11 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( requestedTextureMemory); } - uint32 width = MaximumOctreeTextureWidth; uint32 maximumTileCount = pVoxelComponent->_pDataTextures ? pVoxelComponent->_pDataTextures->getMaximumTileCount() : 1; - pVoxelComponent->_pOctree->initializeTexture(width, maximumTileCount); + pVoxelComponent->_pOctree = MakeUnique(maximumTileCount); pVoxelComponent->_loadedNodeIds.reserve(maximumTileCount); CreateGltfOptions::CreateVoxelOptions& options = pVoxelComponent->Options; diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h index f852bf369..794339b2d 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h @@ -96,7 +96,6 @@ class UCesiumVoxelRendererComponent : public USceneComponent { /** * Value constants taken from CesiumJS. */ - static const uint32 MaximumOctreeTextureWidth = 2048; static const uint32 MaximumDataTextureMemoryBytes = 512 * 1024 * 1024; static const uint32 DefaultDataTextureMemoryBytes = 128 * 1024 * 1024; diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index 95c84494a..1e11a7058 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -10,9 +10,9 @@ using namespace CesiumGeometry; using namespace Cesium3DTilesContent; -void UCesiumVoxelOctreeTexture::update(const std::vector& data) { +bool UCesiumVoxelOctreeTexture::update(const std::vector& data) { if (!this->_pResource || !this->_pResource->TextureRHI) { - return; + return false; } // Compute the area of the texture that actually needs updating. @@ -42,6 +42,7 @@ void UCesiumVoxelOctreeTexture::update(const std::vector& data) { // Pitch = size in bytes of each row of the source image uint32 sourcePitch = region.Width * sizeof(uint32); + ENQUEUE_RENDER_COMMAND(Cesium_UpdateResource) ([pResource = this->_pResource.Get(), &data, region, sourcePitch]( FRHICommandListImmediate& RHICmdList) { @@ -53,11 +54,12 @@ void UCesiumVoxelOctreeTexture::update(const std::vector& data) { reinterpret_cast(data.data())); }); - // this->_fence.BeginFence(); + return true; } /*static*/ UCesiumVoxelOctreeTexture* -UCesiumVoxelOctreeTexture::create(uint32 Width, uint32 MaximumTileCount) { +UCesiumVoxelOctreeTexture::create(uint32 MaximumTileCount) { + const uint32 Width = MaximumOctreeTextureWidth; uint32 TilesPerRow = Width / TexelsPerNode; float Height = (float)MaximumTileCount / (float)TilesPerRow; Height = static_cast(FMath::CeilToInt64(Height)); @@ -68,7 +70,7 @@ UCesiumVoxelOctreeTexture::create(uint32 Width, uint32 MaximumTileCount) { TextureGroup::TEXTUREGROUP_8BitData, Width, Height, - 1, // Depth + 1, /* Depth */ EPixelFormat::PF_R8G8B8A8, TextureFilter::TF_Nearest, TextureAddress::TA_Clamp, @@ -97,43 +99,25 @@ UCesiumVoxelOctreeTexture::create(uint32 Width, uint32 MaximumTileCount) { return pTexture; } -bool UCesiumVoxelOctreeTexture::isReadyToDestroy() const { - return this->_fence.IsFenceComplete(); -} - size_t FVoxelOctree::OctreeTileIDHash::operator()( const CesiumGeometry::OctreeTileID& tileId) const { - // Tiles with the same morton index, but on different levels, are - // distinguished by an offset. This offset is equal to the number of tiles on - // the levels above it, which is the sum of a series, where n = tile.level - - // 1: + // Tiles with the same morton index on different levels are distinguished by + // an offset. This offset is equal to the total number of tiles on the levels + // above it, i.e., the sum of a series where n = tile.level - 1: // 1 + 8 + 8^2 + ... + 8^n = (8^(n+1) - 1) / (8 - 1) - // e.g., for TileID(2, 0, 0, 0), the morton index is 0, but the hash = 9. + // For example, TileID(2, 0, 0, 0) has a morton index of 0, but it hashes + // to 9. size_t levelOffset = tileId.level > 0 ? (std::pow(8, tileId.level) - 1) / 7 : 0; return levelOffset + ImplicitTilingUtilities::computeMortonIndex(tileId); } -FVoxelOctree::FVoxelOctree() : _nodes() { +FVoxelOctree::FVoxelOctree(uint32 maximumTileCount) + : _nodes(), _pTexture(nullptr), _fence(std::nullopt), _data() { CesiumGeometry::OctreeTileID rootTileID(0, 0, 0, 0); this->_nodes.insert({rootTileID, Node()}); -} - -FVoxelOctree::~FVoxelOctree() { - std::vector empty; - std::swap(this->_octreeData, empty); - - UTexture2D* pTexture = this->_pTexture; - this->_pTexture = nullptr; - - if (IsValid(pTexture)) { - pTexture->RemoveFromRoot(); - CesiumLifetime::destroy(pTexture); - } -} -void FVoxelOctree::initializeTexture(uint32 Width, uint32 MaximumTileCount) { - this->_pTexture = UCesiumVoxelOctreeTexture::create(Width, MaximumTileCount); + this->_pTexture = UCesiumVoxelOctreeTexture::create(maximumTileCount); FTextureResource* pResource = this->_pTexture ? this->_pTexture->GetResource() : nullptr; if (!pResource) { @@ -153,6 +137,19 @@ void FVoxelOctree::initializeTexture(uint32 Width, uint32 MaximumTileCount) { }); } +FVoxelOctree::~FVoxelOctree() { + std::vector empty; + std::swap(this->_data, empty); + + UTexture2D* pTexture = this->_pTexture; + this->_pTexture = nullptr; + + if (IsValid(pTexture)) { + pTexture->RemoveFromRoot(); + CesiumLifetime::destroy(pTexture); + } +} + const FVoxelOctree::Node* FVoxelOctree::getNode(const CesiumGeometry::OctreeTileID& TileID) const { return this->_nodes.contains(TileID) ? &this->_nodes.at(TileID) : nullptr; @@ -174,16 +171,16 @@ bool FVoxelOctree::createNode(const CesiumGeometry::OctreeTileID& TileID) { pNode = &this->_nodes[TileID]; // Starting from the target node, traverse the tree upwards and create the - // missing nodes. Stop when we've found an existing parent node. - CesiumGeometry::OctreeTileID currentTileID = TileID; + // missing ancestors. Stop when we've found an existing parent node. + OctreeTileID currentTileID = TileID; bool foundExistingParent = false; + for (uint32_t level = TileID.level; level > 0; level--) { - CesiumGeometry::OctreeTileID parentTileID = - *computeParentTileID(currentTileID); + OctreeTileID parentTileID = + *ImplicitTilingUtilities::getParentID(currentTileID); if (this->_nodes.contains(parentTileID)) { foundExistingParent = true; } else { - // Parent doesn't exist, so create it. this->_nodes.insert({parentTileID, Node()}); } @@ -230,10 +227,9 @@ bool FVoxelOctree::removeNode(const CesiumGeometry::OctreeTileID& tileId) { // There may be cases where the children rely on the parent for rendering. // If so, the node's data cannot be easily released. // TODO: can you also attempt to destroy the node? - CesiumGeometry::OctreeTileID parentTileId = *computeParentTileID(tileId); - Cesium3DTilesContent::OctreeChildren siblings = - ImplicitTilingUtilities::getChildren(parentTileId); - for (const CesiumGeometry::OctreeTileID& siblingId : siblings) { + OctreeTileID parentTileId = *ImplicitTilingUtilities::getParentID(tileId); + OctreeChildren siblings = ImplicitTilingUtilities::getChildren(parentTileId); + for (const OctreeTileID& siblingId : siblings) { if (siblingId == tileId) continue; @@ -250,7 +246,7 @@ bool FVoxelOctree::removeNode(const CesiumGeometry::OctreeTileID& tileId) { } // Otherwise, okay to remove the nodes. - for (const CesiumGeometry::OctreeTileID& siblingId : siblings) { + for (const OctreeTileID& siblingId : siblings) { this->_nodes.erase(this->_nodes.find(siblingId)); } this->getNode(parentTileId)->hasChildren = false; @@ -269,19 +265,7 @@ bool FVoxelOctree::isNodeRenderable( return false; } - return pNode->isDataReady || pNode->hasChildren; -} - -/*static*/ std::optional -FVoxelOctree::computeParentTileID(const CesiumGeometry::OctreeTileID& TileID) { - if (TileID.level == 0) { - return std::nullopt; - } - return CesiumGeometry::OctreeTileID( - TileID.level - 1, - TileID.x >> 1, - TileID.y >> 1, - TileID.z >> 1); + return pNode->dataIndex > 0 || pNode->hasChildren; } /*static*/ void FVoxelOctree::insertNodeData( @@ -381,31 +365,37 @@ void FVoxelOctree::encodeNode( } void FVoxelOctree::updateTexture() { - if (!this->_pTexture) { + if (!this->_pTexture || (this->_fence && !this->_fence->IsFenceComplete())) { return; } - this->_octreeData.clear(); + this->_fence.reset(); + this->_data.clear(); + uint32_t nodeCount = 0; encodeNode( CesiumGeometry::OctreeTileID(0, 0, 0, 0), - this->_octreeData, + this->_data, nodeCount, - 0, - 0, - 0, - 0); + 0, /* octreeIndex */ + 0, /* textureIndex */ + 0, /* parentOctreeIndex */ + 0); /* parentTextureIndex */ // Pad the data as necessary for the texture copy. uint32 regionWidth = this->_pTexture->getTilesPerRow() * UCesiumVoxelOctreeTexture::TexelsPerNode * sizeof(uint32); - uint32 regionHeight = - glm::ceil((float)this->_octreeData.size() / regionWidth); + uint32 regionHeight = glm::ceil((float)this->_data.size() / regionWidth); uint32 expectedSize = regionWidth * regionHeight; - if (this->_octreeData.size() != expectedSize) { - this->_octreeData.resize(expectedSize, std::byte(0)); + + if (this->_data.size() != expectedSize) { + this->_data.resize(expectedSize, std::byte(0)); } - this->_pTexture->update(this->_octreeData); + if (this->_pTexture->update(this->_data)) { + // Prevent changes to the data while the texture is updating on the render + // thread. + this->_fence.emplace().BeginFence(); + } } diff --git a/Source/CesiumRuntime/Private/VoxelOctree.h b/Source/CesiumRuntime/Private/VoxelOctree.h index d1e132964..5641677c2 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.h +++ b/Source/CesiumRuntime/Private/VoxelOctree.h @@ -7,102 +7,93 @@ #include "RenderCommandFence.h" #include -#include #include #include +/** + * A texture that encodes information from \FVoxelOctree. + */ class UCesiumVoxelOctreeTexture : public UTexture2D { public: /** - * @brief Constant representing the number of texels used to represent a node - * in the octree texture. + * @brief The number of texels used to represent a node in the texture. * * The first texel is used to store an index to the node's parent. The * remaining eight represent the indices of the node's children. */ static const uint32 TexelsPerNode = 9; - static UCesiumVoxelOctreeTexture* - create(uint32 Width, uint32 MaximumTileCount); + /** + * @brief The maximum allowed width for the texture. Value taken from + * CesiumJS. + */ + static const uint32 MaximumOctreeTextureWidth = 2048; /** - * Updates the octree texture with the new input data. + * @brief Creates a new texture with the specified tile capacity. */ - void update(const std::vector& data); + static UCesiumVoxelOctreeTexture* create(uint32 MaximumTileCount); + /** + * @brief Gets the number of tiles encoded in a single row of the texture. + */ uint32_t getTilesPerRow() const { return this->_tilesPerRow; } - bool isReadyToDestroy() const; + /** + * @brief Updates the octree texture with the new input data. + * + * @returns True if the update succeeded, false otherwise. + */ + bool update(const std::vector& data); private: FCesiumTextureResourceUniquePtr _pResource; uint32_t _tilesPerRow; - FRenderCommandFence _fence; }; /** - * @brief A representation of an implicitly tiled octree containing voxel - * values. + * @brief A representation of an implicit octree tileset containing voxels. + * + * This is relevant to the raycasted approach for rendering voxels and + * is meant to be paired with \ref VoxelDataTextures. The structure of + * the voxel tileset is communicated to the shader through a texture. + * Tiles with renderable data are linked to slots in \ref VoxelDataTextures. + * + * The connection with \ref VoxelDataTextures is managed externally by + * \UCesiumVoxelRendererComponent. */ class FVoxelOctree { public: /** - * @brief A representation of a tile in an implicitly tiled octree. + * @brief A tile in an implicitly tiled octree. */ struct Node { - Node* pParent; - bool hasChildren; - double lastKnownScreenSpaceError; - int64_t dataIndex; - bool isDataReady; - - Node() - : pParent(nullptr), - hasChildren(false), - lastKnownScreenSpaceError(0.0), - dataIndex(-1), - isDataReady(false) {} - }; - - /** - * @brief An enum that indicates the type of a node encoded on the GPU. - * Indicates what the numerical data value represents for that node. - */ - enum class ENodeFlag : uint8 { /** - * Empty leaf node that should be skipped when rendering. - * - * This may happen if a node's sibling is renderable, but neither it nor its - * parent are renderable, which can happen Native's algorithm loads higher - * LOD tiles before their ancestors. + * @brief Points to the parent of the node, if it exists. */ - Empty = 0, + Node* pParent = nullptr; /** - * Renderable leaf node with two possibilities: - * - * 1. The leaf node has its own data. The encoded data value refers to an - * index in the data texture of the slot containing the voxel tile's data. - * - * 2. The leaf node has no data of its own but is forced to render (such as - * when its siblings are renderable but it is not). The leaf will attempt to - * render the data of the nearest ancestor. The encoded data value refers to - * an index in the data texture of the slot containing the ancestor voxel - * tile's data. - * - * The latter is a unique case that contains an extra packed value -- the - * level difference from the nearest renderable ancestor. This is so the - * rendering implementation can deduce the correct texture coordinates. If - * the leaf node contains its own data, then this value is 0. + * @brief Whether the tile's children exist in the octree. */ - Leaf = 1, + bool hasChildren = false; /** - * Internal node. The encoded data value refers to an index in the octree - * texture where its full representation is located. + * @brief The tile's last known screen space error. */ - Internal = 2, + double lastKnownScreenSpaceError = 0.0; + /** + * @brief The index of the slot that this tile occupies in \ref + * VoxelDataTextures, if any. + */ + int64_t dataIndex = -1; + bool isDataReady = false; }; - FVoxelOctree(); + /** + * @brief Constructs an initially empty octree with the specified tile + * capacity. + */ + FVoxelOctree(uint32 maximumTileCount); + ~FVoxelOctree(); /** @@ -114,10 +105,7 @@ class FVoxelOctree { const Node* getNode(const CesiumGeometry::OctreeTileID& TileID) const; /** - * @brief Gets a node in the octree at the specified tile ID. Returns nullptr - * if it does not exist. - * - * @param TileID The octree tile ID. + * @copydoc getNode */ Node* getNode(const CesiumGeometry::OctreeTileID& TileID); @@ -136,6 +124,7 @@ class FVoxelOctree { * @brief Attempts to remove the node at the specified tile ID. * * This will fail to remove the node from the tree if: + * * - the node is the root of the tree * - the node has renderable siblings * @@ -144,10 +133,6 @@ class FVoxelOctree { */ bool removeNode(const CesiumGeometry::OctreeTileID& TileID); - bool isNodeRenderable(const CesiumGeometry::OctreeTileID& TileID) const; - - void initializeTexture(uint32 width, uint32 maximumTileCount); - /** * @brief Retrieves the texture containing the encoded octree. */ @@ -157,14 +142,42 @@ class FVoxelOctree { private: /** - * @brief Retrieves the tile ID for the parent of the given tile. Does not - * validate whether either tile exists in the octree. - * - * @returns The parent tile ID, or std::nullopt if the given tile is a root - * tile (i.e., level 0). + * @brief An enum that indicates the type of a node encoded on the GPU. + * Indicates what the numerical data value represents for that node. */ - static std::optional - computeParentTileID(const CesiumGeometry::OctreeTileID& TileID); + enum class ENodeFlag : uint8 { + /** + * Empty leaf node that should be skipped when rendering. + * + * This may happen if a node's sibling is renderable, but neither it nor its + * parent are renderable, which can happen Native's algorithm loads higher + * LOD tiles before their ancestors. + */ + Empty = 0, + /** + * Renderable leaf node with two possibilities: + * + * 1. The leaf node has its own data. The encoded data value refers to an + * index in the data texture of the slot containing the voxel tile's data. + * + * 2. The leaf node has no data of its own but is forced to render (such as + * when its siblings are renderable but it is not). The leaf will attempt to + * render the data of the nearest ancestor. The encoded data value refers to + * an index in the data texture of the slot containing the ancestor voxel + * tile's data. + * + * The latter is a unique case that contains an extra packed value -- the + * level difference from the nearest renderable ancestor. This is so the + * rendering implementation can deduce the correct texture coordinates. If + * the leaf node contains its own data, then this value is 0. + */ + Leaf = 1, + /** + * Internal node. The encoded data value refers to an index in the octree + * texture where its full representation is located. + */ + Internal = 2, + }; /** * @brief Inserts the input values to the data vector, automatically @@ -231,31 +244,37 @@ class FVoxelOctree { uint32 parentOctreeIndex, uint32 parentTextureIndex); + bool isNodeRenderable(const CesiumGeometry::OctreeTileID& TileID) const; + struct OctreeTileIDHash { size_t operator()(const CesiumGeometry::OctreeTileID& tileId) const; }; - // This implementation is inspired by Linear (hashed) Octrees: - // https://geidav.wordpress.com/2014/08/18/advanced-octrees-2-node-representations/ - // - // Nodes must track their parent / child relationships so that the tree - // structure can be encoded to a texture, for voxel raymarching. However, - // nodes must also be easily created and/or accessed. cesium-native passes - // tiles over in a vector without spatial organization. Typical tree - // queries are O(log(n)) where n = # tree levels. This is unideal, since it's - // likely that multiple tiles will be made visible in an update based on - // movement. - // - // The compromise: a hashmap that stores octree nodes based on their tile ID. - // The nodes don't point to any children themselves; instead, they store a - // bool indicating whether or not children have been created for them. It's on - // the octree to properly manage this. + /** + * This implementation is inspired by Linear (hashed) Octrees: + * https://geidav.wordpress.com/2014/08/18/advanced-octrees-2-node-representations/ + * + * Nodes must track their parent / child relationships so that the tree + * structure can be encoded to a texture, for voxel raymarching. However, + * nodes must also be easily created and/or accessed. cesium-native passes + * tiles over in a vector without spatial organization. Typical tree + * queries are O(log(n)) where n = # tree levels. This is unideal, since it's + * likely that multiple tiles will be made visible in an update based on + * movement. + * + * The compromise: a hashmap that stores octree nodes based on their tile ID. + * The nodes don't point to any children themselves; instead, they store a + * bool indicating whether or not children have been created for them. It's on + * the octree to properly manage this. + */ using NodeMap = std::unordered_map; NodeMap _nodes; UCesiumVoxelOctreeTexture* _pTexture; + std::optional _fence; + // As the octree grows, save the allocated memory so that recomputing the // same-size octree won't require more allocations. - std::vector _octreeData; + std::vector _data; }; From ea3b491ba2f958a70f2ea8973a3d3b59866b504a Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 26 Jun 2025 11:37:59 -0400 Subject: [PATCH 18/34] Add WIP destroy checks --- .../Private/CesiumMetadataValue.cpp | 1 + .../Private/CesiumVoxelRendererComponent.cpp | 20 ++++++++----- .../Private/VoxelDataTextures.cpp | 30 +++---------------- .../CesiumRuntime/Private/VoxelDataTextures.h | 5 ++-- Source/CesiumRuntime/Private/VoxelOctree.cpp | 13 ++++++-- Source/CesiumRuntime/Private/VoxelOctree.h | 14 +++++---- extern/cesium-native | 2 +- 7 files changed, 40 insertions(+), 45 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumMetadataValue.cpp b/Source/CesiumRuntime/Private/CesiumMetadataValue.cpp index 59b7b315b..a54a45024 100644 --- a/Source/CesiumRuntime/Private/CesiumMetadataValue.cpp +++ b/Source/CesiumRuntime/Private/CesiumMetadataValue.cpp @@ -3,6 +3,7 @@ #include "CesiumMetadataValue.h" #include "CesiumPropertyArray.h" #include "UnrealMetadataConversions.h" + #include #include diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index b37292299..ac20802d5 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -3,6 +3,7 @@ #include "CesiumVoxelRendererComponent.h" #include "CalcBounds.h" #include "Cesium3DTileset.h" +#include "CesiumGltfComponent.h" #include "CesiumLifetime.h" #include "CesiumMaterialUserData.h" #include "CesiumRuntime.h" @@ -59,15 +60,19 @@ void UCesiumVoxelRendererComponent::BeginDestroy() { // Reset the pointers. this->MeshComponent = nullptr; - this->_pOctree.Reset(); - // this->_pDataTextures->BeginDestroy(); Super::BeginDestroy(); } bool UCesiumVoxelRendererComponent::IsReadyForFinishDestroy() { + if (this->_pOctree.IsValid() && !this->_pOctree->canBeDestroyed()) { + return false; + } + if (this->_pDataTextures.IsValid()) { + return this->_pDataTextures->canBeDestroyed(); } + return Super::IsReadyForFinishDestroy(); } @@ -549,9 +554,9 @@ void forEachRenderableVoxelTile(const auto& tiles, Func&& f) { continue; } - UCesiumGltfComponent* Gltf = static_cast( + UCesiumGltfComponent* pGltf = static_cast( pRenderContent->getRenderResources()); - if (!Gltf) { + if (!pGltf) { // When a tile does not have render resources (i.e. a glTF), then // the resources either have not yet been loaded or prepared, // or the tile is from an external tileset and does not directly @@ -559,7 +564,7 @@ void forEachRenderableVoxelTile(const auto& tiles, Func&& f) { continue; } - const TArray& Children = Gltf->GetAttachChildren(); + const TArray& Children = pGltf->GetAttachChildren(); for (USceneComponent* pChild : Children) { UCesiumGltfVoxelComponent* pVoxelComponent = Cast(pChild); @@ -632,8 +637,9 @@ void UCesiumVoxelRendererComponent::UpdateTiles( FVoxelOctree::Node* pNode = this->_pOctree->getNode(currentTileId); if (pNode && pNode->dataIndex >= 0) { // Node has already been loaded into the data textures. - pNode->isDataReady = - this->_pDataTextures->isSlotLoaded(pNode->dataIndex); + bool loaded = this->_pDataTextures->isSlotLoaded(pNode->dataIndex); + shouldUpdateOctree = loaded != pNode->isDataReady; + pNode->isDataReady = loaded; continue; } diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp index 06ae88e16..1b1e83d2f 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp @@ -165,32 +165,10 @@ FVoxelDataTextures::FVoxelDataTextures( FVoxelDataTextures::~FVoxelDataTextures() {} -// void FVoxelDataTextures::BeginDestroy() { -// for (auto propertyIt : this->_propertyMap) { -// UTexture* pTexture = propertyIt.Value.pTexture; -// pTexture->BeginDestroy(); -// propertyIt.Value.pTexture = nullptr; -// propertyIt.Value.pResource = nullptr; -// -// if (IsValid(pTexture)) { -// pTexture->RemoveFromRoot(); -// CesiumLifetime::destroy(pTexture); -// } -// } -// -// this->_propertyMap.Empty(); -// -// Super::BeginDestroy(); -//} -// -// bool FVoxelDataTextures::IsReadyForFinishDestroy() { -// // for (auto propertyIt : this->_propertyMap) { -// // UTexture* pTexture = propertyIt.Value.pTexture; -// // if (pTexture && !pTexture->IsReadyForFinishDestroy()) -// // return false; -// // } -// return Super::IsReadyForFinishDestroy(); -//} +bool FVoxelDataTextures::canBeDestroyed() const { + // TODO + // return true; +} UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { const TextureData* pProperty = this->_propertyMap.Find(attributeId); diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.h b/Source/CesiumRuntime/Private/VoxelDataTextures.h index ac59f2806..83086e93e 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.h +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.h @@ -46,9 +46,6 @@ class FVoxelDataTextures { ~FVoxelDataTextures(); - // virtual void BeginDestroy() override; - // virtual bool IsReadyForFinishDestroy() override; - /** * @brief Gets the maximum number of tiles that can be added to the data * textures. Equivalent to the maximum number of data slots. @@ -83,6 +80,8 @@ class FVoxelDataTextures { */ bool isSlotLoaded(int64 index) const; + bool canBeDestroyed() const; + /** * @brief Attempts to add the voxel tile to the data textures. * diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index 1e11a7058..25a49d8c6 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -138,6 +138,8 @@ FVoxelOctree::FVoxelOctree(uint32 maximumTileCount) } FVoxelOctree::~FVoxelOctree() { + CESIUM_ASSERT(!this->_fence || this->_fence->IsFenceComplete()); + std::vector empty; std::swap(this->_data, empty); @@ -364,9 +366,9 @@ void FVoxelOctree::encodeNode( } } -void FVoxelOctree::updateTexture() { +bool FVoxelOctree::updateTexture() { if (!this->_pTexture || (this->_fence && !this->_fence->IsFenceComplete())) { - return; + return false; } this->_fence.reset(); @@ -397,5 +399,12 @@ void FVoxelOctree::updateTexture() { // Prevent changes to the data while the texture is updating on the render // thread. this->_fence.emplace().BeginFence(); + return true; } + + return false; +} + +bool FVoxelOctree::canBeDestroyed() const { + return this->_fence ? this->_fence->IsFenceComplete() : true; } diff --git a/Source/CesiumRuntime/Private/VoxelOctree.h b/Source/CesiumRuntime/Private/VoxelOctree.h index 5641677c2..88612d8a1 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.h +++ b/Source/CesiumRuntime/Private/VoxelOctree.h @@ -18,8 +18,8 @@ class UCesiumVoxelOctreeTexture : public UTexture2D { /** * @brief The number of texels used to represent a node in the texture. * - * The first texel is used to store an index to the node's parent. The - * remaining eight represent the indices of the node's children. + * The first texel stores an index to the node's parent. The remaining eight + * represent the indices of the node's children. */ static const uint32 TexelsPerNode = 9; @@ -55,11 +55,11 @@ class UCesiumVoxelOctreeTexture : public UTexture2D { * @brief A representation of an implicit octree tileset containing voxels. * * This is relevant to the raycasted approach for rendering voxels and - * is meant to be paired with \ref VoxelDataTextures. The structure of + * is meant to be paired with \ref FVoxelDataTextures. The structure of * the voxel tileset is communicated to the shader through a texture. - * Tiles with renderable data are linked to slots in \ref VoxelDataTextures. + * Tiles with renderable data are linked to slots in \ref FVoxelDataTextures. * - * The connection with \ref VoxelDataTextures is managed externally by + * The connection with \ref FVoxelDataTextures is managed externally by * \UCesiumVoxelRendererComponent. */ class FVoxelOctree { @@ -138,7 +138,9 @@ class FVoxelOctree { */ UTexture2D* getTexture() const { return this->_pTexture; } - void updateTexture(); + bool updateTexture(); + + bool canBeDestroyed() const; private: /** diff --git a/extern/cesium-native b/extern/cesium-native index 9ed5a7aa1..453eef9ef 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 9ed5a7aa1a8474e9ef657c3b67fda00db9497e5f +Subproject commit 453eef9ef975833154c0fd17b9d286c25d805934 From f38741f4c5d42d934a25c2909b32caa7d8011a0a Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 26 Jun 2025 15:45:21 -0400 Subject: [PATCH 19/34] Add polling behavior for proper octree updates --- .../Private/CesiumVoxelRendererComponent.cpp | 28 +++++++++++------- .../Private/CesiumVoxelRendererComponent.h | 3 ++ .../Private/VoxelDataTextures.cpp | 29 ++++++++++++------- .../CesiumRuntime/Private/VoxelDataTextures.h | 28 +++++++++++++----- Source/CesiumRuntime/Private/VoxelOctree.h | 8 ++++- 5 files changed, 68 insertions(+), 28 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index ac20802d5..a1891edbb 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -70,12 +70,20 @@ bool UCesiumVoxelRendererComponent::IsReadyForFinishDestroy() { } if (this->_pDataTextures.IsValid()) { + this->_pDataTextures->pollLoadingSlots(); return this->_pDataTextures->canBeDestroyed(); } return Super::IsReadyForFinishDestroy(); } +void UCesiumVoxelRendererComponent::FinishDestroy() { + this->_pOctree.Reset(); + this->_pDataTextures.Reset(); + + Super::FinishDestroy(); +} + namespace { EVoxelGridShape getVoxelGridShape( const Cesium3DTilesSelection::BoundingVolume& boundingVolume) { @@ -622,8 +630,6 @@ void UCesiumVoxelRendererComponent::UpdateTiles( computePriority(pRight->lastKnownScreenSpaceError); }); - bool shouldUpdateOctree = false; - size_t existingNodeCount = this->_loadedNodeIds.size(); size_t destroyedNodeCount = 0; size_t addedNodeCount = 0; @@ -637,9 +643,8 @@ void UCesiumVoxelRendererComponent::UpdateTiles( FVoxelOctree::Node* pNode = this->_pOctree->getNode(currentTileId); if (pNode && pNode->dataIndex >= 0) { // Node has already been loaded into the data textures. - bool loaded = this->_pDataTextures->isSlotLoaded(pNode->dataIndex); - shouldUpdateOctree = loaded != pNode->isDataReady; - pNode->isDataReady = loaded; + pNode->isDataReady = + this->_pDataTextures->isSlotLoaded(pNode->dataIndex); continue; } @@ -669,7 +674,8 @@ void UCesiumVoxelRendererComponent::UpdateTiles( // Attempt to remove the node and simplify the octree. // Will not succeed if the node's siblings are renderable, or if this // node contains renderable children. - shouldUpdateOctree |= this->_pOctree->removeNode(lowestPriorityId); + this->_needsOctreeUpdate |= + this->_pOctree->removeNode(lowestPriorityId); } else { addNodeIndex = existingNodeCount + addedNodeCount; addedNodeCount++; @@ -682,7 +688,7 @@ void UCesiumVoxelRendererComponent::UpdateTiles( pNode->dataIndex = this->_pDataTextures->add(*pVoxel); bool addedToDataTexture = (pNode->dataIndex >= 0); - shouldUpdateOctree |= createdNewNode || addedToDataTexture; + this->_needsOctreeUpdate |= createdNewNode || addedToDataTexture; if (!addedToDataTexture) { continue; @@ -692,6 +698,8 @@ void UCesiumVoxelRendererComponent::UpdateTiles( this->_loadedNodeIds.push_back(currentTileId); } } + + this->_needsOctreeUpdate |= this->_pDataTextures->pollLoadingSlots(); } else { // If there are no data textures, then for all of the visible nodes... for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { @@ -699,7 +707,7 @@ void UCesiumVoxelRendererComponent::UpdateTiles( const CesiumGeometry::OctreeTileID& currentTileId = currentTile.pComponent->TileId; // Create the node if it does not already exist in the tree. - shouldUpdateOctree |= this->_pOctree->createNode(currentTileId); + this->_needsOctreeUpdate |= this->_pOctree->createNode(currentTileId); FVoxelOctree::Node* pNode = this->_pOctree->getNode(currentTileId); pNode->lastKnownScreenSpaceError = currentTile.sse; @@ -710,8 +718,8 @@ void UCesiumVoxelRendererComponent::UpdateTiles( } } - if (shouldUpdateOctree) { - this->_pOctree->updateTexture(); + if (this->_needsOctreeUpdate) { + this->_needsOctreeUpdate = this->_pOctree->updateTexture(); } } diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h index 794339b2d..2b37eee20 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h @@ -53,6 +53,7 @@ class UCesiumVoxelRendererComponent : public USceneComponent { void BeginDestroy() override; bool IsReadyForFinishDestroy() override; + void FinishDestroy() override; UPROPERTY(EditAnywhere, Category = "Cesium") UMaterialInterface* DefaultMaterial = nullptr; @@ -145,4 +146,6 @@ class UCesiumVoxelRendererComponent : public USceneComponent { * The tileset that owns this voxel renderer. */ ACesium3DTileset* _pTileset = nullptr; + + bool _needsOctreeUpdate; }; diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp index 1b1e83d2f..6f501ac6e 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp @@ -26,6 +26,7 @@ FVoxelDataTextures::FVoxelDataTextures( ERHIFeatureLevel::Type featureLevel, uint32 requestedMemoryPerTexture) : _slots(), + _loadingSlots(), _pEmptySlotsHead(nullptr), _pOccupiedSlotsHead(nullptr), _dataDimensions(dataDimensions), @@ -166,8 +167,7 @@ FVoxelDataTextures::FVoxelDataTextures( FVoxelDataTextures::~FVoxelDataTextures() {} bool FVoxelDataTextures::canBeDestroyed() const { - // TODO - // return true; + return this->_loadingSlots.size() == 0; } UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { @@ -270,14 +270,6 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { }); } -bool FVoxelDataTextures::isSlotLoaded(int64 index) const { - if (index < 0 || index >= int64(this->_slots.size())) - return false; - - return this->_slots[size_t(index)].fence && - this->_slots[size_t(index)].fence->IsFenceComplete(); -} - int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { int64 slotIndex = this->reserveNextSlot(); if (slotIndex < 0) { @@ -323,6 +315,7 @@ int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { } this->_slots[slotIndex].fence.emplace().BeginFence(); + this->_loadingSlots.insert(slotIndex); return slotIndex; } @@ -376,3 +369,19 @@ int64 FVoxelDataTextures::reserveNextSlot() { return pSlot->index; } + +bool FVoxelDataTextures::isSlotLoaded(int64 index) const { + if (index < 0 || index >= int64(this->_slots.size())) + return false; + + return this->_slots[size_t(index)].fence && + this->_slots[size_t(index)].fence->IsFenceComplete(); +} + +bool FVoxelDataTextures::pollLoadingSlots() { + size_t loadingSlotCount = this->_loadingSlots.size(); + std::erase_if(this->_loadingSlots, [thiz = this](size_t i) { + return thiz->isSlotLoaded(i); + }); + return loadingSlotCount != this->_loadingSlots.size(); +} diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.h b/Source/CesiumRuntime/Private/VoxelDataTextures.h index 83086e93e..8af3d2512 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.h +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.h @@ -6,8 +6,10 @@ #include "CesiumMetadataValueType.h" #include "EncodedFeaturesMetadata.h" #include "RenderCommandFence.h" + #include #include +#include struct FCesiumVoxelClassDescription; class FCesiumTextureResource; @@ -75,13 +77,6 @@ class FVoxelDataTextures { */ bool isFull() const { return this->_pEmptySlotsHead == nullptr; } - /** - * @brief Whether or not the slot at the given index is loaded. - */ - bool isSlotLoaded(int64 index) const; - - bool canBeDestroyed() const; - /** * @brief Attempts to add the voxel tile to the data textures. * @@ -95,6 +90,23 @@ class FVoxelDataTextures { */ bool release(int64_t slotIndex); + /** + * @brief Whether or not the slot at the given index has loaded data. + */ + bool isSlotLoaded(int64 index) const; + + /** + * @brief Whether the textures can be destroyed. Returns false if there are + * any render thread commands in flight. + */ + bool canBeDestroyed() const; + + /** + * @brief Checks the progress of slots with data being loaded into the + * megatexture. Retusn true if any slots completed loading. + */ + bool pollLoadingSlots(); + private: /** * @brief Represents a slot in the voxel data texture that contains a single @@ -166,6 +178,8 @@ class FVoxelDataTextures { int64_t reserveNextSlot(); std::vector _slots; + std::unordered_set _loadingSlots; + Slot* _pEmptySlotsHead; Slot* _pOccupiedSlotsHead; diff --git a/Source/CesiumRuntime/Private/VoxelOctree.h b/Source/CesiumRuntime/Private/VoxelOctree.h index 88612d8a1..fad675c25 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.h +++ b/Source/CesiumRuntime/Private/VoxelOctree.h @@ -82,9 +82,15 @@ class FVoxelOctree { double lastKnownScreenSpaceError = 0.0; /** * @brief The index of the slot that this tile occupies in \ref - * VoxelDataTextures, if any. + * FVoxelDataTextures, if any. */ int64_t dataIndex = -1; + /** + * @brief Whether this tile's data is actually loaded into \ref + * FVoxelDataTextures. Although the node may have an assigned slot index, + * the data must be uploaded to the texture during the render thread, and + * thus won't be immediately available to render. + */ bool isDataReady = false; }; From 59e8b37dd32bf383c8031656613acfed6396e02a Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 27 Jun 2025 18:07:09 -0400 Subject: [PATCH 20/34] Fix extra destroy bug --- CHANGES.md | 6 + .../Private/CesiumVoxelRendererComponent.cpp | 7 - .../Private/CesiumVoxelRendererComponent.h | 1 - .../Private/VoxelDataTextures.cpp | 62 ++- .../CesiumRuntime/Private/VoxelDataTextures.h | 9 - Source/CesiumRuntime/Private/VoxelOctree.cpp | 374 ++++++++---------- Source/CesiumRuntime/Private/VoxelOctree.h | 228 ++++++----- 7 files changed, 319 insertions(+), 368 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index aa0279b5b..61920e748 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ # Change Log {#changes} +### ? - ? + +##### Additions :tada: + +- Added support for tilesets containing `3DTILES_content_voxels`. Voxel metadata can be styled with materials generated by `UCesiumVoxelMetadataComponent`. + ### v2.16.1 - 2025-06-02 ##### Additions :tada: diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index a1891edbb..3e362b512 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -77,13 +77,6 @@ bool UCesiumVoxelRendererComponent::IsReadyForFinishDestroy() { return Super::IsReadyForFinishDestroy(); } -void UCesiumVoxelRendererComponent::FinishDestroy() { - this->_pOctree.Reset(); - this->_pDataTextures.Reset(); - - Super::FinishDestroy(); -} - namespace { EVoxelGridShape getVoxelGridShape( const Cesium3DTilesSelection::BoundingVolume& boundingVolume) { diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h index 2b37eee20..3d2203369 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h @@ -53,7 +53,6 @@ class UCesiumVoxelRendererComponent : public USceneComponent { void BeginDestroy() override; bool IsReadyForFinishDestroy() override; - void FinishDestroy() override; UPROPERTY(EditAnywhere, Category = "Cesium") UMaterialInterface* DefaultMaterial = nullptr; diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp index 6f501ac6e..605369406 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.cpp @@ -74,7 +74,7 @@ FVoxelDataTextures::FVoxelDataTextures( encodedFormat.channels * encodedFormat.bytesPerChannel; this->_propertyMap.Add( Property.Name, - {encodedFormat, texelSizeBytes, nullptr, nullptr}); + {encodedFormat, texelSizeBytes, nullptr}); maximumTexelSizeBytes = FMath::Max(maximumTexelSizeBytes, texelSizeBytes); } @@ -124,18 +124,17 @@ FVoxelDataTextures::FVoxelDataTextures( // Create the actual textures. for (auto& propertyIt : this->_propertyMap) { - FCesiumTextureResource* pTextureResource = - FCesiumTextureResource::CreateEmpty( - TextureGroup::TEXTUREGROUP_8BitData, - actualDimensions.x, - actualDimensions.y, - actualDimensions.z, - propertyIt.Value.encodedFormat.format, - TextureFilter::TF_Nearest, - TextureAddress::TA_Clamp, - TextureAddress::TA_Clamp, - false) - .Release(); + FTextureResource* pResource = FCesiumTextureResource::CreateEmpty( + TextureGroup::TEXTUREGROUP_8BitData, + actualDimensions.x, + actualDimensions.y, + actualDimensions.z, + propertyIt.Value.encodedFormat.format, + TextureFilter::TF_Nearest, + TextureAddress::TA_Clamp, + TextureAddress::TA_Clamp, + false) + .Release(); UVolumeTexture* pTexture = NewObject( GetTransientPackage(), @@ -149,14 +148,15 @@ FVoxelDataTextures::FVoxelDataTextures( pTexture->SRGB = false; pTexture->NeverStream = true; - pTexture->SetResource(pTextureResource); - + pTexture->SetResource(pResource); propertyIt.Value.pTexture = pTexture; - propertyIt.Value.pResource = pTextureResource; ENQUEUE_RENDER_COMMAND(Cesium_InitResource) - ([pTexture, - pResource = pTextureResource](FRHICommandListImmediate& RHICmdList) { + ([pTexture, pResource = pTexture->GetResource()]( + FRHICommandListImmediate& RHICmdList) { + if (!pResource) + return; + pResource->SetTextureReference( pTexture->TextureReference.TextureReferenceRHI); pResource->InitResource(FRHICommandListImmediate::Get()); @@ -164,7 +164,9 @@ FVoxelDataTextures::FVoxelDataTextures( } } -FVoxelDataTextures::~FVoxelDataTextures() {} +FVoxelDataTextures::~FVoxelDataTextures() { + CESIUM_ASSERT(this->canBeDestroyed()); +} bool FVoxelDataTextures::canBeDestroyed() const { return this->_loadingSlots.size() == 0; @@ -184,7 +186,7 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { const FCesiumPropertyAttributeProperty& property, const FVoxelDataTextures::TextureData& data, const FUpdateTextureRegion3D& updateRegion) { - if (!data.pResource || !data.pTexture) + if (!data.pTexture) return; const uint8* pData = @@ -192,15 +194,15 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { ENQUEUE_RENDER_COMMAND(Cesium_DirectCopyVoxels) ([pTexture = data.pTexture, - pResource = data.pResource, format = data.encodedFormat.format, &property, updateRegion, texelSizeBytes = data.texelSizeBytes, pData](FRHICommandListImmediate& RHICmdList) { - if (!IsValid(pTexture)) { + FTextureResource* pResource = + IsValid(pTexture) ? pTexture->GetResource() : nullptr; + if (!pResource) return; - } // Pitch = size in bytes of each row of the source image. uint32 srcRowPitch = updateRegion.Width * texelSizeBytes; @@ -221,24 +223,20 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { const FCesiumPropertyAttributeProperty& property, const FVoxelDataTextures::TextureData& data, const FUpdateTextureRegion3D& updateRegion) { - if (!data.pResource || !data.pTexture) + if (!data.pTexture) return; ENQUEUE_RENDER_COMMAND(Cesium_IncrementalWriteVoxels) ([pTexture = data.pTexture, - pResource = data.pResource, format = data.encodedFormat.format, &property, updateRegion, texelSizeBytes = data.texelSizeBytes](FRHICommandListImmediate& RHICmdList) { - // We're trusting that Cesium3DTileset will destroy its attached - // CesiumVoxelRendererComponent (and thus the VoxelDataTextures) - // before unloading glTFs. As long as the texture is valid, so is the - // CesiumPropertyAttributeProperty. - if (!IsValid(pTexture)) { + FTextureResource* pResource = + IsValid(pTexture) ? pTexture->GetResource() : nullptr; + if (!pResource) return; - } FUpdateTexture3DData UpdateData = RHIBeginUpdateTexture3D(pResource->TextureRHI, 0, updateRegion); @@ -295,7 +293,7 @@ int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { uint32 index = static_cast(slotIndex); - for (auto PropertyIt : this->_propertyMap) { + for (const auto& PropertyIt : this->_propertyMap) { const FCesiumPropertyAttributeProperty& property = UCesiumPropertyAttributeBlueprintLibrary::FindProperty( voxelComponent.PropertyAttribute, diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.h b/Source/CesiumRuntime/Private/VoxelDataTextures.h index 8af3d2512..d0cfe3ba5 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.h +++ b/Source/CesiumRuntime/Private/VoxelDataTextures.h @@ -137,15 +137,6 @@ class FVoxelDataTextures { * @brief The data texture for this property. */ UTexture* pTexture; - - /** - * @brief A pointer to the texture resource. There is no way to retrieve - * this through the UTexture API, so the pointer is stored here. - * - * Although this would ideally be a TUniquePtr, it prevents TMap from - * compiling. - */ - FCesiumTextureResource* pResource; }; /** diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index 25a49d8c6..cfcf0f1c7 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -3,6 +3,7 @@ #include "VoxelOctree.h" #include "CesiumLifetime.h" #include "CesiumRuntime.h" +#include "CesiumTextureResource.h" #include #include @@ -10,13 +11,87 @@ using namespace CesiumGeometry; using namespace Cesium3DTilesContent; -bool UCesiumVoxelOctreeTexture::update(const std::vector& data) { - if (!this->_pResource || !this->_pResource->TextureRHI) { - return false; +/*static*/ UVoxelOctreeTexture* +UVoxelOctreeTexture::create(uint32 maximumTileCount) { + const uint32 width = MaximumOctreeTextureWidth; + uint32 tilesPerRow = width / TexelsPerNode; + float height = (float)maximumTileCount / (float)tilesPerRow; + height = static_cast(FMath::CeilToInt64(height)); + height = FMath::Clamp(height, 1, MaximumOctreeTextureWidth); + + FTextureResource* pResource = FCesiumTextureResource::CreateEmpty( + TextureGroup::TEXTUREGROUP_8BitData, + MaximumOctreeTextureWidth, + height, + 1, /* Depth */ + EPixelFormat::PF_R8G8B8A8, + TextureFilter::TF_Nearest, + TextureAddress::TA_Clamp, + TextureAddress::TA_Clamp, + false) + .Release(); + + UVoxelOctreeTexture* pTexture = NewObject( + GetTransientPackage(), + MakeUniqueObjectName( + GetTransientPackage(), + UTexture2D::StaticClass(), + "VoxelOctreeTexture"), + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + + pTexture->AddressX = TextureAddress::TA_Clamp; + pTexture->AddressY = TextureAddress::TA_Clamp; + pTexture->Filter = TextureFilter::TF_Nearest; + pTexture->LODGroup = TextureGroup::TEXTUREGROUP_8BitData; + pTexture->SRGB = false; + pTexture->NeverStream = true; + + if (!pTexture || !pResource) { + UE_LOG( + LogCesium, + Error, + TEXT("Could not create texture for voxel octree.")); + return nullptr; + } + + pTexture->SetResource(pResource); + pTexture->_tilesPerRow = tilesPerRow; + + ENQUEUE_RENDER_COMMAND(Cesium_InitResource) + ([pTexture, + pResource = pTexture->GetResource()](FRHICommandListImmediate& RHICmdList) { + pResource->SetTextureReference( + pTexture->TextureReference.TextureReferenceRHI); + pResource->InitResource(FRHICommandListImmediate::Get()); + }); + + return pTexture; +} + +void UVoxelOctreeTexture::update(const FVoxelOctree& octree) { + this->_data.clear(); + + uint32_t nodeCount = 0; + encodeNode( + octree, + CesiumGeometry::OctreeTileID(0, 0, 0, 0), + nodeCount, + 0, /* octreeIndex */ + 0, /* textureIndex */ + 0, /* parentOctreeIndex */ + 0); /* parentTextureIndex */ + + // Pad the data as necessary for the texture copy. + uint32 regionWidth = this->_tilesPerRow * TexelsPerNode * sizeof(uint32); + uint32 regionHeight = glm::ceil((float)this->_data.size() / regionWidth); + uint32 expectedSize = regionWidth * regionHeight; + + if (this->_data.size() != expectedSize) { + this->_data.resize(expectedSize, std::byte(0)); } // Compute the area of the texture that actually needs updating. - uint32 texelCount = data.size() / sizeof(uint32); + uint32 texelCount = this->_data.size() / sizeof(uint32); uint32 tileCount = texelCount / TexelsPerNode; glm::uvec2 updateExtent; @@ -37,14 +112,15 @@ bool UCesiumVoxelOctreeTexture::update(const std::vector& data) { region.SrcX = 0; region.SrcY = 0; - region.Width = FMath::Clamp(region.Width, 1, this->_pResource->GetSizeX()); - region.Height = FMath::Clamp(region.Height, 1, this->_pResource->GetSizeY()); + region.Width = FMath::Clamp(region.Width, 1, this->GetResource()->GetSizeX()); + region.Height = + FMath::Clamp(region.Height, 1, this->GetResource()->GetSizeY()); // Pitch = size in bytes of each row of the source image uint32 sourcePitch = region.Width * sizeof(uint32); ENQUEUE_RENDER_COMMAND(Cesium_UpdateResource) - ([pResource = this->_pResource.Get(), &data, region, sourcePitch]( + ([pResource = this->GetResource(), &data = this->_data, region, sourcePitch]( FRHICommandListImmediate& RHICmdList) { RHIUpdateTexture2D( pResource->TextureRHI, @@ -53,50 +129,94 @@ bool UCesiumVoxelOctreeTexture::update(const std::vector& data) { sourcePitch, reinterpret_cast(data.data())); }); - - return true; } -/*static*/ UCesiumVoxelOctreeTexture* -UCesiumVoxelOctreeTexture::create(uint32 MaximumTileCount) { - const uint32 Width = MaximumOctreeTextureWidth; - uint32 TilesPerRow = Width / TexelsPerNode; - float Height = (float)MaximumTileCount / (float)TilesPerRow; - Height = static_cast(FMath::CeilToInt64(Height)); - Height = FMath::Clamp(Height, 1, Width); - - FCesiumTextureResourceUniquePtr pResource = - FCesiumTextureResource::CreateEmpty( - TextureGroup::TEXTUREGROUP_8BitData, - Width, - Height, - 1, /* Depth */ - EPixelFormat::PF_R8G8B8A8, - TextureFilter::TF_Nearest, - TextureAddress::TA_Clamp, - TextureAddress::TA_Clamp, - false); - - UCesiumVoxelOctreeTexture* pTexture = NewObject( - GetTransientPackage(), - MakeUniqueObjectName( - GetTransientPackage(), - UTexture2D::StaticClass(), - "VoxelOctreeTexture"), - RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); +void UVoxelOctreeTexture::insertNodeData( + uint32 textureIndex, + ENodeFlag nodeFlag, + uint16 data, + uint8 renderableLevelDifference) { + uint32_t dataIndex = textureIndex * sizeof(uint32_t); + if (this->_data.size() < dataIndex + sizeof(uint32_t)) { + this->_data.resize(dataIndex + sizeof(uint32_t)); + } - pTexture->AddressX = TextureAddress::TA_Clamp; - pTexture->AddressY = TextureAddress::TA_Clamp; - pTexture->Filter = TextureFilter::TF_Nearest; - pTexture->LODGroup = TextureGroup::TEXTUREGROUP_8BitData; - pTexture->SRGB = false; - pTexture->NeverStream = true; + // Explicitly encode the values in little endian order. + this->_data[dataIndex] = std::byte(nodeFlag); + this->_data[dataIndex + 1] = std::byte(renderableLevelDifference); + this->_data[dataIndex + 2] = std::byte(data & 0x00ff); + this->_data[dataIndex + 3] = std::byte(data >> 8); +}; + +void UVoxelOctreeTexture::encodeNode( + const FVoxelOctree& octree, + const CesiumGeometry::OctreeTileID& tileId, + uint32& nodeCount, + uint32 octreeIndex, + uint32 textureIndex, + uint32 parentOctreeIndex, + uint32 parentTextureIndex) { + const FVoxelOctree::Node* pNode = octree.getNode(tileId); + CESIUM_ASSERT(pNode); - pTexture->_tilesPerRow = TilesPerRow; - pTexture->_pResource = std::move(pResource); - pTexture->SetResource(pTexture->_pResource.Get()); + if (pNode->hasChildren) { + // Point the parent and child octree indices at each other + insertNodeData(parentTextureIndex, ENodeFlag::Internal, octreeIndex); + insertNodeData(textureIndex, ENodeFlag::Internal, parentOctreeIndex); + nodeCount++; - return pTexture; + // Continue traversing + parentOctreeIndex = octreeIndex; + parentTextureIndex = parentOctreeIndex * TexelsPerNode + 1; + + uint32 childIndex = 0; + for (const CesiumGeometry::OctreeTileID& childId : + ImplicitTilingUtilities::getChildren(tileId)) { + octreeIndex = nodeCount; + textureIndex = octreeIndex * TexelsPerNode; + + encodeNode( + octree, + childId, + nodeCount, + octreeIndex, + textureIndex, + parentOctreeIndex, + parentTextureIndex + childIndex++); + } + } else { + // Leaf nodes involve more complexity. + ENodeFlag flag = ENodeFlag::Empty; + uint16 value = 0; + uint16 levelDifference = 0; + + if (pNode->isDataReady) { + flag = ENodeFlag::Leaf; + value = static_cast(pNode->dataIndex); + } else if (pNode->pParent) { + FVoxelOctree::Node* pParent = pNode->pParent; + + for (uint32 levelsAbove = 1; levelsAbove <= tileId.level; levelsAbove++) { + if (pParent->isDataReady) { + flag = ENodeFlag::Leaf; + value = static_cast(pParent->dataIndex); + levelDifference = levelsAbove; + break; + } + + // Continue trying to find a renderable ancestor. + pParent = pParent->pParent; + + if (!pParent) { + // This happens if we've reached the root node and it's not + // renderable. + break; + } + } + } + insertNodeData(parentTextureIndex, flag, value, levelDifference); + nodeCount++; + } } size_t FVoxelOctree::OctreeTileIDHash::operator()( @@ -116,40 +236,11 @@ FVoxelOctree::FVoxelOctree(uint32 maximumTileCount) : _nodes(), _pTexture(nullptr), _fence(std::nullopt), _data() { CesiumGeometry::OctreeTileID rootTileID(0, 0, 0, 0); this->_nodes.insert({rootTileID, Node()}); - - this->_pTexture = UCesiumVoxelOctreeTexture::create(maximumTileCount); - FTextureResource* pResource = - this->_pTexture ? this->_pTexture->GetResource() : nullptr; - if (!pResource) { - UE_LOG( - LogCesium, - Error, - TEXT("Could not create texture for voxel octree.")); - return; - } - - ENQUEUE_RENDER_COMMAND(Cesium_InitResource) - ([pTexture = this->_pTexture, - pResource](FRHICommandListImmediate& RHICmdList) { - pResource->SetTextureReference( - pTexture->TextureReference.TextureReferenceRHI); - pResource->InitResource(FRHICommandListImmediate::Get()); - }); + this->_pTexture = UVoxelOctreeTexture::create(maximumTileCount); } FVoxelOctree::~FVoxelOctree() { CESIUM_ASSERT(!this->_fence || this->_fence->IsFenceComplete()); - - std::vector empty; - std::swap(this->_data, empty); - - UTexture2D* pTexture = this->_pTexture; - this->_pTexture = nullptr; - - if (IsValid(pTexture)) { - pTexture->RemoveFromRoot(); - CesiumLifetime::destroy(pTexture); - } } const FVoxelOctree::Node* @@ -270,139 +361,18 @@ bool FVoxelOctree::isNodeRenderable( return pNode->dataIndex > 0 || pNode->hasChildren; } -/*static*/ void FVoxelOctree::insertNodeData( - std::vector& nodeData, - uint32 textureIndex, - FVoxelOctree::ENodeFlag nodeFlag, - uint16 data, - uint8 renderableLevelDifference) { - uint32 dataIndex = textureIndex * sizeof(uint32); - if (nodeData.size() <= dataIndex) { - nodeData.resize(dataIndex + sizeof(uint32), std::byte(0)); - } - // Explicitly encode the values in little endian order. - nodeData[dataIndex] = std::byte(nodeFlag); - nodeData[dataIndex + 1] = std::byte(renderableLevelDifference); - nodeData[dataIndex + 2] = std::byte(data & 0x00ff); - nodeData[dataIndex + 3] = std::byte(data >> 8); -}; - -void FVoxelOctree::encodeNode( - const CesiumGeometry::OctreeTileID& tileId, - std::vector& nodeData, - uint32& nodeCount, - uint32 octreeIndex, - uint32 textureIndex, - uint32 parentOctreeIndex, - uint32 parentTextureIndex) { - constexpr uint32 TexelsPerNode = UCesiumVoxelOctreeTexture::TexelsPerNode; - - Node* pNode = &this->_nodes[tileId]; - if (pNode->hasChildren) { - // Point the parent and child octree indices at each other - insertNodeData( - nodeData, - parentTextureIndex, - ENodeFlag::Internal, - octreeIndex); - insertNodeData( - nodeData, - textureIndex, - ENodeFlag::Internal, - parentOctreeIndex); - nodeCount++; - - // Continue traversing - parentOctreeIndex = octreeIndex; - parentTextureIndex = parentOctreeIndex * TexelsPerNode + 1; - - uint32 childIndex = 0; - for (const CesiumGeometry::OctreeTileID& childId : - ImplicitTilingUtilities::getChildren(tileId)) { - octreeIndex = nodeCount; - textureIndex = octreeIndex * TexelsPerNode; - - encodeNode( - childId, - nodeData, - nodeCount, - octreeIndex, - textureIndex, - parentOctreeIndex, - parentTextureIndex + childIndex++); - } - } else { - // Leaf nodes involve more complexity. - ENodeFlag flag = ENodeFlag::Empty; - uint16 value = 0; - uint16 levelDifference = 0; - - if (pNode->isDataReady) { - flag = ENodeFlag::Leaf; - value = static_cast(pNode->dataIndex); - } else if (pNode->pParent) { - FVoxelOctree::Node* pParent = pNode->pParent; - - for (uint32 levelsAbove = 1; levelsAbove <= tileId.level; levelsAbove++) { - if (pParent->isDataReady) { - flag = ENodeFlag::Leaf; - value = static_cast(pParent->dataIndex); - levelDifference = levelsAbove; - break; - } - - // Continue trying to find a renderable ancestor. - pParent = pParent->pParent; - - if (!pParent) { - // This happens if we've reached the root node and it's not - // renderable. - break; - } - } - } - insertNodeData(nodeData, parentTextureIndex, flag, value, levelDifference); - nodeCount++; - } -} - bool FVoxelOctree::updateTexture() { if (!this->_pTexture || (this->_fence && !this->_fence->IsFenceComplete())) { return false; } this->_fence.reset(); - this->_data.clear(); + this->_pTexture->update(*this); - uint32_t nodeCount = 0; - encodeNode( - CesiumGeometry::OctreeTileID(0, 0, 0, 0), - this->_data, - nodeCount, - 0, /* octreeIndex */ - 0, /* textureIndex */ - 0, /* parentOctreeIndex */ - 0); /* parentTextureIndex */ - - // Pad the data as necessary for the texture copy. - uint32 regionWidth = this->_pTexture->getTilesPerRow() * - UCesiumVoxelOctreeTexture::TexelsPerNode * - sizeof(uint32); - uint32 regionHeight = glm::ceil((float)this->_data.size() / regionWidth); - uint32 expectedSize = regionWidth * regionHeight; - - if (this->_data.size() != expectedSize) { - this->_data.resize(expectedSize, std::byte(0)); - } - - if (this->_pTexture->update(this->_data)) { - // Prevent changes to the data while the texture is updating on the render - // thread. - this->_fence.emplace().BeginFence(); - return true; - } - - return false; + // Prevent changes to the data while the texture is updating on the render + // thread. + this->_fence.emplace().BeginFence(); + return true; } bool FVoxelOctree::canBeDestroyed() const { diff --git a/Source/CesiumRuntime/Private/VoxelOctree.h b/Source/CesiumRuntime/Private/VoxelOctree.h index fad675c25..419a74147 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.h +++ b/Source/CesiumRuntime/Private/VoxelOctree.h @@ -2,7 +2,6 @@ #pragma once -#include "CesiumTextureResource.h" #include "Engine/Texture2D.h" #include "RenderCommandFence.h" @@ -10,11 +9,24 @@ #include #include +class FVoxelOctree; + /** - * A texture that encodes information from \FVoxelOctree. + * A texture that encodes information from \ref FVoxelOctree. */ -class UCesiumVoxelOctreeTexture : public UTexture2D { +class UVoxelOctreeTexture : public UTexture2D { public: + /** + * @brief Creates a new texture with the specified tile capacity. + */ + static UVoxelOctreeTexture* create(uint32 maximumTileCount); + + /** + * @brief Updates the texture, capturing the structure of the given octree. + */ + void update(const FVoxelOctree& octree); + +private: /** * @brief The number of texels used to represent a node in the texture. * @@ -30,25 +42,109 @@ class UCesiumVoxelOctreeTexture : public UTexture2D { static const uint32 MaximumOctreeTextureWidth = 2048; /** - * @brief Creates a new texture with the specified tile capacity. + * @brief An enum that indicates the type of a node encoded on the GPU. + * Indicates what the numerical data value represents for that node. */ - static UCesiumVoxelOctreeTexture* create(uint32 MaximumTileCount); + enum class ENodeFlag : uint8 { + /** + * Empty leaf node that should be skipped when rendering. + * + * This may happen if a node's sibling is renderable, but neither it nor its + * parent are renderable, which can happen Native's algorithm loads higher + * LOD tiles before their ancestors. + */ + Empty = 0, + /** + * Renderable leaf node with two possibilities: + * + * 1. The leaf node has its own data. The encoded data value refers to an + * index in the data texture of the slot containing the voxel tile's data. + * + * 2. The leaf node has no data of its own but is forced to render (such as + * when its siblings are renderable but it is not). The leaf will attempt to + * render the data of the nearest ancestor. The encoded data value refers to + * an index in the data texture of the slot containing the ancestor voxel + * tile's data. + * + * The latter is a unique case that contains an extra packed value -- the + * level difference from the nearest renderable ancestor. This is so the + * rendering implementation can deduce the correct texture coordinates. If + * the leaf node contains its own data, then this value is 0. + */ + Leaf = 1, + /** + * Internal node. The encoded data value refers to an index in the octree + * texture where its full representation is located. + */ + Internal = 2, + }; /** - * @brief Gets the number of tiles encoded in a single row of the texture. + * @brief Inserts the input values to the texture's data vector, automatically + * expanding it if the target index is out-of-bounds. */ - uint32_t getTilesPerRow() const { return this->_tilesPerRow; } + void insertNodeData( + uint32 textureIndex, + ENodeFlag nodeFlag, + uint16 data, + uint8 renderableLevelDifference = 0); /** - * @brief Updates the octree texture with the new input data. + * @brief Recursively writes octree nodes as their expected representation + * in the GPU texture. * - * @returns True if the update succeeded, false otherwise. + * Example Below (shown as binary tree instead of octree for + * demonstration purposes) + * + * Tree: + * 0 + * / \ + * / \ + * / \ + * 1 3 + * / \ / \ + * L0 2 L3 L4 + * / \ + * L1 L2 + * + * + * GPU Array: + * L = leaf index + * * = index to parent node + * node index: 0_______ 1________ 2________ 3_________ + * data array: [*0, 1, 3, *0, L0, 2, *1 L1, L2, *0, L3, L4] + * + * The array is generated from a depth-first traversal. The end result could + * be an unbalanced tree, so the parent index is stored at each node to make + * it possible to traverse upwards. + * + * Nodes are indexed by the order in which they appear in the traversal. + * + * @param octree The voxel octree. + * @param tileId The ID of the node to be encoded. + * @param nodeCount The current number of encoded numbers, used to assign + * indices to each node. Accumulates over all calls of this function. + * @param octreeIndex The index of the node relative to all the nodes + * encountered in the octree, based on nodeCount. Acts as the identifier for + * the node. + * @param textureIndex The texel index of the node within the texture where + * the node will be expanded if it is an internal node. + * @param parentOctreeIndex The octree index of the parent. + * @param parentTextureIndex The texel index of the node within the texture + * where the node data will be written, relative to the parent's texel + * index. */ - bool update(const std::vector& data); + void encodeNode( + const FVoxelOctree& octree, + const CesiumGeometry::OctreeTileID& tileId, + uint32& nodeCount, + uint32 octreeIndex, + uint32 textureIndex, + uint32 parentOctreeIndex, + uint32 parentTextureIndex); -private: - FCesiumTextureResourceUniquePtr _pResource; - uint32_t _tilesPerRow; + uint32 _tilesPerRow; + std::vector _data; }; /** @@ -149,109 +245,6 @@ class FVoxelOctree { bool canBeDestroyed() const; private: - /** - * @brief An enum that indicates the type of a node encoded on the GPU. - * Indicates what the numerical data value represents for that node. - */ - enum class ENodeFlag : uint8 { - /** - * Empty leaf node that should be skipped when rendering. - * - * This may happen if a node's sibling is renderable, but neither it nor its - * parent are renderable, which can happen Native's algorithm loads higher - * LOD tiles before their ancestors. - */ - Empty = 0, - /** - * Renderable leaf node with two possibilities: - * - * 1. The leaf node has its own data. The encoded data value refers to an - * index in the data texture of the slot containing the voxel tile's data. - * - * 2. The leaf node has no data of its own but is forced to render (such as - * when its siblings are renderable but it is not). The leaf will attempt to - * render the data of the nearest ancestor. The encoded data value refers to - * an index in the data texture of the slot containing the ancestor voxel - * tile's data. - * - * The latter is a unique case that contains an extra packed value -- the - * level difference from the nearest renderable ancestor. This is so the - * rendering implementation can deduce the correct texture coordinates. If - * the leaf node contains its own data, then this value is 0. - */ - Leaf = 1, - /** - * Internal node. The encoded data value refers to an index in the octree - * texture where its full representation is located. - */ - Internal = 2, - }; - - /** - * @brief Inserts the input values to the data vector, automatically - * expanding it if the target index is out-of-bounds. - */ - static void insertNodeData( - std::vector& nodeData, - uint32 textureIndex, - ENodeFlag nodeFlag, - uint16 data, - uint8 renderableLevelDifference = 0); - - /** - * @brief Recursively writes octree nodes as their expected representation - * in the GPU texture. - * - * Example Below (shown as binary tree instead of octree for - * demonstration purposes) - * - * Tree: - * 0 - * / \ - * / \ - * / \ - * 1 3 - * / \ / \ - * L0 2 L3 L4 - * / \ - * L1 L2 - * - * - * GPU Array: - * L = leaf index - * * = index to parent node - * node index: 0_______ 1________ 2________ 3_________ - * data array: [*0, 1, 3, *0, L0, 2, *1 L1, L2, *0, L3, L4] - * - * The array is generated from a depth-first traversal. The end result could - * be an unbalanced tree, so the parent index is stored at each node to make - * it possible to traverse upwards. - * - * Nodes are indexed by the order in which they appear in the traversal. - * - * @param pNode The node to be encoded. - * @param nodeData The data buffer to write to. - * @param nodeCount The current number of encoded numbers, used to assign - * indices to each node. Accumulates over all calls of this function. - * @param octreeIndex The index of the node relative to all the nodes - * encountered in the octree, based on nodeCount. Acts as the identifier for - * the node. - * @param textureIndex The texel index of the node within the texture where - * the node will be expanded if it is an internal node. - * @param parentOctreeIndex The octree index of the parent. - * @param parentTextureIndex The texel index of the node within the texture - * where the node data will be written, relative to the parent's texel - * index. - */ - void encodeNode( - const CesiumGeometry::OctreeTileID& tileId, - std::vector& nodeData, - uint32& nodeCount, - uint32 octreeIndex, - uint32 textureIndex, - uint32 parentOctreeIndex, - uint32 parentTextureIndex); - bool isNodeRenderable(const CesiumGeometry::OctreeTileID& TileID) const; struct OctreeTileIDHash { @@ -279,7 +272,8 @@ class FVoxelOctree { std::unordered_map; NodeMap _nodes; - UCesiumVoxelOctreeTexture* _pTexture; + UVoxelOctreeTexture* _pTexture; + uint32_t _tilesPerRow; std::optional _fence; // As the octree grows, save the allocated memory so that recomputing the From d5494f268046607524359c6c44978ac332cd98f2 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 2 Jul 2025 15:56:06 -0400 Subject: [PATCH 21/34] Fix weird memory corruption errors --- .../Private/CesiumVoxelRendererComponent.cpp | 2 +- Source/CesiumRuntime/Private/VoxelOctree.cpp | 66 ++++++++++++------- Source/CesiumRuntime/Private/VoxelOctree.h | 20 ++++-- 3 files changed, 56 insertions(+), 32 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 3e362b512..883590acd 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -712,7 +712,7 @@ void UCesiumVoxelRendererComponent::UpdateTiles( } if (this->_needsOctreeUpdate) { - this->_needsOctreeUpdate = this->_pOctree->updateTexture(); + this->_needsOctreeUpdate = !this->_pOctree->updateTexture(); } } diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index cfcf0f1c7..1f3aeeb47 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -68,30 +68,33 @@ UVoxelOctreeTexture::create(uint32 maximumTileCount) { return pTexture; } -void UVoxelOctreeTexture::update(const FVoxelOctree& octree) { - this->_data.clear(); +void UVoxelOctreeTexture::update( + const FVoxelOctree& octree, + std::vector& result) { + result.clear(); uint32_t nodeCount = 0; encodeNode( octree, CesiumGeometry::OctreeTileID(0, 0, 0, 0), nodeCount, - 0, /* octreeIndex */ - 0, /* textureIndex */ - 0, /* parentOctreeIndex */ - 0); /* parentTextureIndex */ + 0, /* octreeIndex */ + 0, /* textureIndex */ + 0, /* parentOctreeIndex */ + 0, + result); /* parentTextureIndex */ // Pad the data as necessary for the texture copy. uint32 regionWidth = this->_tilesPerRow * TexelsPerNode * sizeof(uint32); - uint32 regionHeight = glm::ceil((float)this->_data.size() / regionWidth); + uint32 regionHeight = glm::ceil((float)result.size() / regionWidth); uint32 expectedSize = regionWidth * regionHeight; - if (this->_data.size() != expectedSize) { - this->_data.resize(expectedSize, std::byte(0)); + if (result.size() != expectedSize) { + result.resize(expectedSize, std::byte(0)); } // Compute the area of the texture that actually needs updating. - uint32 texelCount = this->_data.size() / sizeof(uint32); + uint32 texelCount = result.size() / sizeof(uint32); uint32 tileCount = texelCount / TexelsPerNode; glm::uvec2 updateExtent; @@ -120,32 +123,35 @@ void UVoxelOctreeTexture::update(const FVoxelOctree& octree) { uint32 sourcePitch = region.Width * sizeof(uint32); ENQUEUE_RENDER_COMMAND(Cesium_UpdateResource) - ([pResource = this->GetResource(), &data = this->_data, region, sourcePitch]( + ([pResource = this->GetResource(), &result, region, sourcePitch]( FRHICommandListImmediate& RHICmdList) { RHIUpdateTexture2D( pResource->TextureRHI, 0, region, sourcePitch, - reinterpret_cast(data.data())); + reinterpret_cast(result.data())); }); } void UVoxelOctreeTexture::insertNodeData( + std::vector& data, uint32 textureIndex, ENodeFlag nodeFlag, - uint16 data, + uint16 dataValue, uint8 renderableLevelDifference) { uint32_t dataIndex = textureIndex * sizeof(uint32_t); - if (this->_data.size() < dataIndex + sizeof(uint32_t)) { - this->_data.resize(dataIndex + sizeof(uint32_t)); + + const size_t desiredSize = dataIndex + sizeof(uint32_t); + if (data.size() < desiredSize) { + data.resize(desiredSize); } // Explicitly encode the values in little endian order. - this->_data[dataIndex] = std::byte(nodeFlag); - this->_data[dataIndex + 1] = std::byte(renderableLevelDifference); - this->_data[dataIndex + 2] = std::byte(data & 0x00ff); - this->_data[dataIndex + 3] = std::byte(data >> 8); + data[dataIndex] = std::byte(nodeFlag); + data[dataIndex + 1] = std::byte(renderableLevelDifference); + data[dataIndex + 2] = std::byte(dataValue & 0x00ff); + data[dataIndex + 3] = std::byte(dataValue >> 8); }; void UVoxelOctreeTexture::encodeNode( @@ -155,14 +161,23 @@ void UVoxelOctreeTexture::encodeNode( uint32 octreeIndex, uint32 textureIndex, uint32 parentOctreeIndex, - uint32 parentTextureIndex) { + uint32 parentTextureIndex, + std::vector& result) { const FVoxelOctree::Node* pNode = octree.getNode(tileId); CESIUM_ASSERT(pNode); if (pNode->hasChildren) { // Point the parent and child octree indices at each other - insertNodeData(parentTextureIndex, ENodeFlag::Internal, octreeIndex); - insertNodeData(textureIndex, ENodeFlag::Internal, parentOctreeIndex); + insertNodeData( + result, + parentTextureIndex, + ENodeFlag::Internal, + octreeIndex); + insertNodeData( + result, + textureIndex, + ENodeFlag::Internal, + parentOctreeIndex); nodeCount++; // Continue traversing @@ -182,7 +197,8 @@ void UVoxelOctreeTexture::encodeNode( octreeIndex, textureIndex, parentOctreeIndex, - parentTextureIndex + childIndex++); + parentTextureIndex + childIndex++, + result); } } else { // Leaf nodes involve more complexity. @@ -214,7 +230,7 @@ void UVoxelOctreeTexture::encodeNode( } } } - insertNodeData(parentTextureIndex, flag, value, levelDifference); + insertNodeData(result, parentTextureIndex, flag, value, levelDifference); nodeCount++; } } @@ -367,7 +383,7 @@ bool FVoxelOctree::updateTexture() { } this->_fence.reset(); - this->_pTexture->update(*this); + this->_pTexture->update(*this, this->_data); // Prevent changes to the data while the texture is updating on the render // thread. diff --git a/Source/CesiumRuntime/Private/VoxelOctree.h b/Source/CesiumRuntime/Private/VoxelOctree.h index 419a74147..9ce6acd52 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.h +++ b/Source/CesiumRuntime/Private/VoxelOctree.h @@ -22,9 +22,16 @@ class UVoxelOctreeTexture : public UTexture2D { static UVoxelOctreeTexture* create(uint32 maximumTileCount); /** - * @brief Updates the texture, capturing the structure of the given octree. + * @brief Updates the texture, encoding the structure of the given octree as + * in the result vector and prompting an update during the render thread. + * + * Storing non-trivial types on `UVoxelOctreeTexture` often results in + * memory corruption when the texture is created. Thus, this requires the + * vector to be externally supplied and managed. It is recommended to use a + * FRenderCommandFence to query when the texture update completes, as to avoid + * destroying the vector mid-update. */ - void update(const FVoxelOctree& octree); + void update(const FVoxelOctree& octree, std::vector& result); private: /** @@ -80,13 +87,14 @@ class UVoxelOctreeTexture : public UTexture2D { }; /** - * @brief Inserts the input values to the texture's data vector, automatically + * @brief Inserts the input values to the given data vector, automatically * expanding it if the target index is out-of-bounds. */ void insertNodeData( + std::vector& data, uint32 textureIndex, ENodeFlag nodeFlag, - uint16 data, + uint16 dataValue, uint8 renderableLevelDifference = 0); /** @@ -141,10 +149,10 @@ class UVoxelOctreeTexture : public UTexture2D { uint32 octreeIndex, uint32 textureIndex, uint32 parentOctreeIndex, - uint32 parentTextureIndex); + uint32 parentTextureIndex, + std::vector& result); uint32 _tilesPerRow; - std::vector _data; }; /** From 8fbf9fdc8ac2563c2aded526f4f5e38ac6e924cf Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 3 Jul 2025 10:28:02 -0400 Subject: [PATCH 22/34] Cleanup --- Source/CesiumRuntime/Private/Cesium3DTileset.cpp | 7 ++++--- Source/CesiumRuntime/Private/CesiumGltfComponent.cpp | 8 ++++---- .../Private/CesiumVoxelRendererComponent.cpp | 1 - .../CesiumRuntime/Private/CesiumVoxelRendererComponent.h | 7 ------- Source/CesiumRuntime/Private/VoxelOctree.cpp | 2 +- Source/CesiumRuntime/Public/Cesium3DTileset.h | 4 ++-- 6 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index af168e3bd..2f76ce756 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -1185,7 +1185,7 @@ void ACesium3DTileset::LoadTileset() { const auto* pVoxelExtension = pExternalContent->getExtension< Cesium3DTiles::ExtensionContent3dTilesContentVoxels>(); if (pVoxelExtension) { - thiz->initializeVoxelRenderer(*pVoxelExtension); + thiz->createVoxelRenderer(*pVoxelExtension); } }); @@ -1288,7 +1288,8 @@ void ACesium3DTileset::DestroyTileset() { } // Tiles are about to be deleted, so we should not keep raw pointers on them. - // This would crash in Tick() when if refresh events were triggered frequently. + // This would crash in Tick() when if refresh events were triggered + // frequently. this->_tilesToHideNextFrame.clear(); if (!this->_pTileset) { @@ -2375,7 +2376,7 @@ void ACesium3DTileset::RuntimeSettingsChanged( } #endif -void ACesium3DTileset::initializeVoxelRenderer( +void ACesium3DTileset::createVoxelRenderer( const Cesium3DTiles::ExtensionContent3dTilesContentVoxels& VoxelExtension) { const Cesium3DTilesSelection::Tile* pRootTile = this->_pTileset->getRootTile(); diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index 98ed141a8..2b651d5ea 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp @@ -1907,12 +1907,12 @@ bool paddingIsConsideredEqual( EVoxelGridShape gridShape, const std::optional& gltfPadding, const std::optional& tilesetPadding) { - if (gltfPadding.has_value() != tilesetPadding.has_value()) { - return false; + if (!gltfPadding && !tilesetPadding) { + return true; } - if (!gltfPadding.has_value() && !tilesetPadding.has_value()) { - return true; + if (gltfPadding.has_value() != tilesetPadding.has_value()) { + return false; } return dimensionsAreConsideredEqual( diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 883590acd..6da031701 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -433,7 +433,6 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( pVoxelComponent->SetMobility(pTilesetActor->GetRootComponent()->Mobility); pVoxelComponent->SetFlags( RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); - pVoxelComponent->_pTileset = pTilesetActor; UStaticMeshComponent* pVoxelMesh = NewObject(pVoxelComponent); diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h index 3d2203369..2a2d50e35 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h @@ -15,7 +15,6 @@ #include #include -#include #include #include @@ -140,11 +139,5 @@ class UCesiumVoxelRendererComponent : public USceneComponent { TUniquePtr _pDataTextures; std::vector _loadedNodeIds; MaxPriorityQueue _visibleTileQueue; - - /** - * The tileset that owns this voxel renderer. - */ - ACesium3DTileset* _pTileset = nullptr; - bool _needsOctreeUpdate; }; diff --git a/Source/CesiumRuntime/Private/VoxelOctree.cpp b/Source/CesiumRuntime/Private/VoxelOctree.cpp index 1f3aeeb47..9f2e71091 100644 --- a/Source/CesiumRuntime/Private/VoxelOctree.cpp +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -15,6 +15,7 @@ using namespace Cesium3DTilesContent; UVoxelOctreeTexture::create(uint32 maximumTileCount) { const uint32 width = MaximumOctreeTextureWidth; uint32 tilesPerRow = width / TexelsPerNode; + float height = (float)maximumTileCount / (float)tilesPerRow; height = static_cast(FMath::CeilToInt64(height)); height = FMath::Clamp(height, 1, MaximumOctreeTextureWidth); @@ -335,7 +336,6 @@ bool FVoxelOctree::removeNode(const CesiumGeometry::OctreeTileID& tileId) { // There may be cases where the children rely on the parent for rendering. // If so, the node's data cannot be easily released. - // TODO: can you also attempt to destroy the node? OctreeTileID parentTileId = *ImplicitTilingUtilities::getParentID(tileId); OctreeChildren siblings = ImplicitTilingUtilities::getChildren(parentTileId); for (const OctreeTileID& siblingId : siblings) { diff --git a/Source/CesiumRuntime/Public/Cesium3DTileset.h b/Source/CesiumRuntime/Public/Cesium3DTileset.h index c82714421..3275fe18f 100644 --- a/Source/CesiumRuntime/Public/Cesium3DTileset.h +++ b/Source/CesiumRuntime/Public/Cesium3DTileset.h @@ -1273,9 +1273,9 @@ class CESIUMRUNTIME_API ACesium3DTileset : public AActor { UCesiumEllipsoid* NewEllpisoid); /** - * Initializes the CesiumVoxelRenderer component for rendering voxel data. + * Creates and attaches a \ref UCesiumVoxelRendererComponent for rendering voxel data. */ - void initializeVoxelRenderer( + void createVoxelRenderer( const Cesium3DTiles::ExtensionContent3dTilesContentVoxels& VoxelExtension); From ed2863751e508fa7e075516e1e597713828ea432 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 3 Jul 2025 10:53:14 -0400 Subject: [PATCH 23/34] Use different native commit --- extern/cesium-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/cesium-native b/extern/cesium-native index 5975ff90e..0050ee440 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 5975ff90ef846c9b22da0ea1c8fa74fa0f4abb0d +Subproject commit 0050ee440c5590c664347cd39fda1a30192942fe From d52a0f9f4497bccc11783a17fffc697907f2a1ac Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 3 Jul 2025 14:55:16 -0400 Subject: [PATCH 24/34] Refactor megatextures to use known tile count --- .../Private/CesiumGltfVoxelComponent.h | 9 +- .../Private/CesiumVoxelRendererComponent.cpp | 74 ++++----------- .../Private/CesiumVoxelRendererComponent.h | 28 ++---- ...DataTextures.cpp => VoxelMegatextures.cpp} | 92 ++++++++++--------- ...oxelDataTextures.h => VoxelMegatextures.h} | 48 +++++----- extern/cesium-native | 2 +- 6 files changed, 106 insertions(+), 147 deletions(-) rename Source/CesiumRuntime/Private/{VoxelDataTextures.cpp => VoxelMegatextures.cpp} (81%) rename Source/CesiumRuntime/Private/{VoxelDataTextures.h => VoxelMegatextures.h} (82%) diff --git a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h index 052990c97..8d9bacaa3 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h +++ b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h @@ -11,11 +11,12 @@ #include "CesiumGltfVoxelComponent.generated.h" /** - * A barebones component representing a glTF voxel primitive. + * A minimal component representing a glTF voxel primitive. * - * This does not hold any mesh data itself; instead, it contains the property - * attribute used.UCesiumVoxelRendererComponent takes care of voxel rendering - * for an entire tileset. + * This component is not a mesh component. Instead, it contains the property + * attribute used for the voxel primitive. It is \ref + * UCesiumVoxelRendererComponent that handles voxel rendering for the entire + * tileset. */ UCLASS() class UCesiumGltfVoxelComponent : public USceneComponent { diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 6da031701..0f725c87d 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -177,29 +177,6 @@ getMetadataValue(const std::optional& jsonValue) { return FCesiumMetadataValue(); } -// uint32 getMaximumTextureMemory( -// const FCesiumVoxelClassDescription* pDescription, -// const glm::uvec3& gridDimensions, -// uint64_t tileCount) { -// int32_t pixelSize = 0; -// -// if (pDescription) { -// for (const FCesiumPropertyAttributePropertyDescription& Property : -// pDescription->Properties) { -// EncodedFeaturesMetadata::EncodedPixelFormat pixelFormat = -// EncodedFeaturesMetadata::getPixelFormat( -// Property.EncodingDetails.Type, -// Property.EncodingDetails.ComponentType); -// pixelSize = FMath::Max( -// pixelSize, -// pixelFormat.bytesPerChannel * pixelFormat.channels); -// } -// } -// -// return (uint32)pixelSize * gridDimensions.x * gridDimensions.y * -// gridDimensions.z * tileCount; -// } - } // namespace /*static*/ UMaterialInstanceDynamic* @@ -469,40 +446,29 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( glm::uvec3(dataDimensions.x, dataDimensions.z, dataDimensions.y); } - uint32 requestedTextureMemory = DefaultDataTextureMemoryBytes; - - // uint64_t knownTileCount = 0; - // if (tilesetMetadata.metadata) { - // const Cesium3DTiles::MetadataEntity& metadata = - // *tilesetMetadata.metadata; - // // TODO: This should find the property by "TILESET_TILE_COUNT" - // if (metadata.properties.find("tileCount") != metadata.properties.end()) - // { - // const CesiumUtility::JsonValue& value = - // metadata.properties.at("tileCount"); - // if (value.isInt64()) { - // knownTileCount = value.getInt64OrDefault(0); - // } else if (value.isUint64()) { - // knownTileCount = value.getUint64OrDefault(0); - // } - // } - // } - - // if (knownTileCount > 0) { - // uint32 maximumTextureMemory = - // getMaximumTextureMemory(pDescription, dataDimensions, - // knownTileCount); - // requestedTextureMemory = FMath::Min( - // maximumTextureMemory, - // FVoxelResources::MaximumDataTextureMemoryBytes); - //} + uint32 knownTileCount = 0; + if (tilesetMetadata.metadata) { + const Cesium3DTiles::MetadataEntity& metadata = *tilesetMetadata.metadata; + const Cesium3DTiles::Class& tilesetClass = + tilesetMetadata.schema->classes.at(metadata.classProperty); + for (const auto propertyIt : tilesetClass.properties) { + if (propertyIt.second.semantic == "TILESET_TILE_COUNT") { + const auto tileCountIt = metadata.properties.find(propertyIt.first); + if (tileCountIt != metadata.properties.end()) { + knownTileCount = + tileCountIt->second.getSafeNumberOrDefault(0); + } + break; + } + } + } if (pDescription && pVoxelMesh->GetScene()) { - pVoxelComponent->_pDataTextures = MakeUnique( - pDescription, + pVoxelComponent->_pDataTextures = MakeUnique( + *pDescription, dataDimensions, pVoxelMesh->GetScene()->GetFeatureLevel(), - requestedTextureMemory); + knownTileCount); } uint32 maximumTileCount = @@ -626,7 +592,7 @@ void UCesiumVoxelRendererComponent::UpdateTiles( size_t destroyedNodeCount = 0; size_t addedNodeCount = 0; - if (this->_pDataTextures != nullptr) { + if (this->_pDataTextures) { // For all of the visible nodes... for (; !this->_visibleTileQueue.empty(); this->_visibleTileQueue.pop()) { const VoxelTileUpdateInfo& currentTile = this->_visibleTileQueue.top(); diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h index 2a2d50e35..411797df1 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h @@ -9,7 +9,7 @@ #include "CustomDepthParameters.h" #include "Materials/MaterialInstanceDynamic.h" #include "Templates/UniquePtr.h" -#include "VoxelDataTextures.h" +#include "VoxelMegatextures.h" #include "VoxelGridShape.h" #include "VoxelOctree.h" @@ -29,11 +29,10 @@ struct FCesiumVoxelClassDescription; UCLASS() /** - * A component that enables raycasted voxel rendering. This is only attached to - * a Cesium3DTileset when it contains voxel data. + * A component that enables raymarched voxel rendering across an entire tileset. * - * Unlike typical triangle meshes, voxels are rendered by raycasting in an - * Unreal material attached to a placeholder cube mesh. + * Unlike triangle meshes, voxels are rendered by raymarching in an Unreal + * material assigned to a placeholder cube mesh. */ class UCesiumVoxelRendererComponent : public USceneComponent { GENERATED_BODY() @@ -92,12 +91,6 @@ class UCesiumVoxelRendererComponent : public USceneComponent { const std::vector& VisibleTileScreenSpaceErrors); private: - /** - * Value constants taken from CesiumJS. - */ - static const uint32 MaximumDataTextureMemoryBytes = 512 * 1024 * 1024; - static const uint32 DefaultDataTextureMemoryBytes = 128 * 1024 * 1024; - static UMaterialInstanceDynamic* CreateVoxelMaterial( UCesiumVoxelRendererComponent* pVoxelComponent, const FVector& dimensions, @@ -116,14 +109,7 @@ class UCesiumVoxelRendererComponent : public USceneComponent { double priority; }; - struct ScreenSpaceErrorGreaterComparator { - bool - operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { - return lhs.priority > rhs.priority; - } - }; - - struct ScreenSpaceErrorLessComparator { + struct PriorityLessComparator { bool operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { return lhs.priority < rhs.priority; @@ -133,10 +119,10 @@ class UCesiumVoxelRendererComponent : public USceneComponent { using MaxPriorityQueue = std::priority_queue< VoxelTileUpdateInfo, std::vector, - ScreenSpaceErrorLessComparator>; + PriorityLessComparator>; TUniquePtr _pOctree; - TUniquePtr _pDataTextures; + TUniquePtr _pDataTextures; std::vector _loadedNodeIds; MaxPriorityQueue _visibleTileQueue; bool _needsOctreeUpdate; diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp b/Source/CesiumRuntime/Private/VoxelMegatextures.cpp similarity index 81% rename from Source/CesiumRuntime/Private/VoxelDataTextures.cpp rename to Source/CesiumRuntime/Private/VoxelMegatextures.cpp index 605369406..1c47dd478 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelMegatextures.cpp @@ -1,6 +1,6 @@ // Copyright 2020-2024 CesiumGS, Inc. and Contributors -#include "VoxelDataTextures.h" +#include "VoxelMegatextures.h" #include "CesiumGltfVoxelComponent.h" #include "CesiumLifetime.h" @@ -16,40 +16,32 @@ #include #include +#include using namespace CesiumGltf; using namespace EncodedFeaturesMetadata; -FVoxelDataTextures::FVoxelDataTextures( - const FCesiumVoxelClassDescription* pVoxelClass, - const glm::uvec3& dataDimensions, +FVoxelMegatextures::FVoxelMegatextures( + const FCesiumVoxelClassDescription& description, + const glm::uvec3& slotDimensions, ERHIFeatureLevel::Type featureLevel, - uint32 requestedMemoryPerTexture) + uint32 knownTileCount) : _slots(), _loadingSlots(), _pEmptySlotsHead(nullptr), _pOccupiedSlotsHead(nullptr), - _dataDimensions(dataDimensions), + _slotDimensions(slotDimensions), _tileCountAlongAxes(0), _maximumTileCount(0), _propertyMap() { - if (!pVoxelClass) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "Voxel tileset is missing a UCesiumVoxelMetadataComponent. Add a UCesiumVoxelMetadataComponent to visualize the metadata within the tileset.")) - return; - } - - if (pVoxelClass->Properties.IsEmpty()) { + if (description.Properties.IsEmpty()) { return; } if (!RHISupportsVolumeTextures(featureLevel)) { // TODO: 2D fallback? Not sure if this check is the same as // SupportsVolumeTextureRendering, which is false on Vulkan Android, Metal, - // and OpenGL + // and OpenGL. UE_LOG( LogCesium, Error, @@ -62,7 +54,7 @@ FVoxelDataTextures::FVoxelDataTextures( // So first, identify which attribute is the largest in size. uint32 maximumTexelSizeBytes = 0; for (const FCesiumPropertyAttributePropertyDescription& Property : - pVoxelClass->Properties) { + description.Properties) { EncodedPixelFormat encodedFormat = getPixelFormat( Property.EncodingDetails.Type, Property.EncodingDetails.ComponentType); @@ -88,14 +80,25 @@ FVoxelDataTextures::FVoxelDataTextures( return; } - uint32 texelCount = requestedMemoryPerTexture / maximumTexelSizeBytes; - uint32 textureDimension = std::cbrtf(static_cast(texelCount)); + uint32 texelsPerSlot = glm::compMul(slotDimensions); + uint32 memoryPerTexture = DefaultTextureMemoryBytes; + if (knownTileCount > 0) { + memoryPerTexture = glm::min( + maximumTexelSizeBytes * texelsPerSlot * knownTileCount, + MaximumTextureMemoryBytes); + } + + uint32 maximumTexelCount = memoryPerTexture / maximumTexelSizeBytes; + + // Find a best fit for the requested memory. Given a target volume + // (maximumTexelCount) and the slot dimensions (xyz), find some scalar that + // fits the dimensions as close as possible. + float scalar = std::cbrtf(float(maximumTexelCount) / float(texelsPerSlot)); + glm::vec3 textureDimensions = glm::round(scalar * glm::vec3(slotDimensions)); - this->_tileCountAlongAxes = - glm::uvec3(textureDimension) / this->_dataDimensions; + this->_tileCountAlongAxes = glm::uvec3(textureDimensions) / slotDimensions; - if (this->_tileCountAlongAxes.x == 0 || this->_tileCountAlongAxes.y == 0 || - this->_tileCountAlongAxes.z == 0) { + if (glm::any(glm::equal(this->_tileCountAlongAxes, glm::uvec3(0)))) { UE_LOG( LogCesium, Error, @@ -104,8 +107,7 @@ FVoxelDataTextures::FVoxelDataTextures( return; } - glm::uvec3 actualDimensions = - this->_tileCountAlongAxes * this->_dataDimensions; + glm::uvec3 actualDimensions = this->_tileCountAlongAxes * slotDimensions; this->_maximumTileCount = this->_tileCountAlongAxes.x * this->_tileCountAlongAxes.y * @@ -164,15 +166,15 @@ FVoxelDataTextures::FVoxelDataTextures( } } -FVoxelDataTextures::~FVoxelDataTextures() { +FVoxelMegatextures::~FVoxelMegatextures() { CESIUM_ASSERT(this->canBeDestroyed()); } -bool FVoxelDataTextures::canBeDestroyed() const { +bool FVoxelMegatextures::canBeDestroyed() const { return this->_loadingSlots.size() == 0; } -UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { +UTexture* FVoxelMegatextures::getTexture(const FString& attributeId) const { const TextureData* pProperty = this->_propertyMap.Find(attributeId); return pProperty ? pProperty->pTexture : nullptr; } @@ -182,9 +184,9 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { * type that the texture expects. Coercive encoding behavior (similar to what * is done for CesiumPropertyTableProperty) could be added in the future. */ -/*static*/ void FVoxelDataTextures::directCopyToTexture( +/*static*/ void FVoxelMegatextures::directCopyToTexture( const FCesiumPropertyAttributeProperty& property, - const FVoxelDataTextures::TextureData& data, + const FVoxelMegatextures::TextureData& data, const FUpdateTextureRegion3D& updateRegion) { if (!data.pTexture) return; @@ -219,9 +221,9 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { }); } -/*static*/ void FVoxelDataTextures::incrementalWriteToTexture( +/*static*/ void FVoxelMegatextures::incrementalWriteToTexture( const FCesiumPropertyAttributeProperty& property, - const FVoxelDataTextures::TextureData& data, + const FVoxelMegatextures::TextureData& data, const FUpdateTextureRegion3D& updateRegion) { if (!data.pTexture) return; @@ -268,7 +270,7 @@ UTexture* FVoxelDataTextures::getTexture(const FString& attributeId) const { }); } -int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { +int64 FVoxelMegatextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { int64 slotIndex = this->reserveNextSlot(); if (slotIndex < 0) { return -1; @@ -276,9 +278,9 @@ int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { // Compute the update region for the data textures. FUpdateTextureRegion3D updateRegion; - updateRegion.Width = this->_dataDimensions.x; - updateRegion.Height = this->_dataDimensions.y; - updateRegion.Depth = this->_dataDimensions.z; + updateRegion.Width = this->_slotDimensions.x; + updateRegion.Height = this->_slotDimensions.y; + updateRegion.Depth = this->_slotDimensions.z; updateRegion.SrcX = 0; updateRegion.SrcY = 0; updateRegion.SrcZ = 0; @@ -287,9 +289,9 @@ int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { uint32 indexZ = slotIndex / zSlice; uint32 indexY = (slotIndex % zSlice) / this->_tileCountAlongAxes.x; uint32 indexX = slotIndex % this->_tileCountAlongAxes.x; - updateRegion.DestZ = indexZ * this->_dataDimensions.z; - updateRegion.DestY = indexY * this->_dataDimensions.y; - updateRegion.DestX = indexX * this->_dataDimensions.x; + updateRegion.DestZ = indexZ * this->_slotDimensions.z; + updateRegion.DestY = indexY * this->_slotDimensions.y; + updateRegion.DestX = indexX * this->_slotDimensions.x; uint32 index = static_cast(slotIndex); @@ -318,7 +320,7 @@ int64 FVoxelDataTextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { return slotIndex; } -bool FVoxelDataTextures::release(int64_t slotIndex) { +bool FVoxelMegatextures::release(int64_t slotIndex) { if (slotIndex < 0 || slotIndex >= int64(this->_slots.size())) { return false; // Index out of bounds } @@ -345,9 +347,9 @@ bool FVoxelDataTextures::release(int64_t slotIndex) { return true; } -int64 FVoxelDataTextures::reserveNextSlot() { +int64 FVoxelMegatextures::reserveNextSlot() { // Remove head from list of empty slots - FVoxelDataTextures::Slot* pSlot = this->_pEmptySlotsHead; + FVoxelMegatextures::Slot* pSlot = this->_pEmptySlotsHead; if (!pSlot) { return -1; } @@ -368,7 +370,7 @@ int64 FVoxelDataTextures::reserveNextSlot() { return pSlot->index; } -bool FVoxelDataTextures::isSlotLoaded(int64 index) const { +bool FVoxelMegatextures::isSlotLoaded(int64 index) const { if (index < 0 || index >= int64(this->_slots.size())) return false; @@ -376,7 +378,7 @@ bool FVoxelDataTextures::isSlotLoaded(int64 index) const { this->_slots[size_t(index)].fence->IsFenceComplete(); } -bool FVoxelDataTextures::pollLoadingSlots() { +bool FVoxelMegatextures::pollLoadingSlots() { size_t loadingSlotCount = this->_loadingSlots.size(); std::erase_if(this->_loadingSlots, [thiz = this](size_t i) { return thiz->isSlotLoaded(i); diff --git a/Source/CesiumRuntime/Private/VoxelDataTextures.h b/Source/CesiumRuntime/Private/VoxelMegatextures.h similarity index 82% rename from Source/CesiumRuntime/Private/VoxelDataTextures.h rename to Source/CesiumRuntime/Private/VoxelMegatextures.h index d0cfe3ba5..c492bf673 100644 --- a/Source/CesiumRuntime/Private/VoxelDataTextures.h +++ b/Source/CesiumRuntime/Private/VoxelMegatextures.h @@ -16,8 +16,9 @@ class FCesiumTextureResource; class UCesiumGltfVoxelComponent; /** - * Manages the data texture resources for a voxel dataset, where - * each data texture represents an attribute. This is responsible for + * Data texture resources for a voxel dataset, with one texture per voxel + * attribute. A data texture is a "megatexture" containing numerous slots, each + * of which can store the data of one voxel primitive. This is responsible for * synchronizing which slots are occupied across all data textures. * * Due to the requirements of voxel rendering (primarily, sampling voxels from @@ -28,25 +29,27 @@ class UCesiumGltfVoxelComponent; * Counterpart to Megatexture.js in CesiumJS, except this takes advantage of 3D * textures to simplify some of the texture read/write math. */ -class FVoxelDataTextures { +class FVoxelMegatextures { public: /** * @brief Constructs a set of voxel data textures. * - * @param pVoxelClass The voxel class description, indicating which metadata + * @param description The voxel class description, indicating which metadata * attributes to encode. - * @param dataDimensions The dimensions of the voxel data, including padding. + * @param slotDimensions The dimensions of each slot (i.e, the voxel grid + * dimensions, including padding). * @param featureLevel The RHI feature level associated with the scene. - * @param requestedMemoryPerTexture The requested texture memory for each - * voxel attribute. + * @param knownTileCount The number of known tiles in the tileset. This + * informs how much texture memory will be allocated for the data textures. If + * this is zero, a default value will be used. */ - FVoxelDataTextures( - const FCesiumVoxelClassDescription* pDescription, - const glm::uvec3& dataDimensions, + FVoxelMegatextures( + const FCesiumVoxelClassDescription& description, + const glm::uvec3& slotDimensions, ERHIFeatureLevel::Type featureLevel, - uint32 requestedMemoryPerTexture); + uint32 knownTileCount); - ~FVoxelDataTextures(); + ~FVoxelMegatextures(); /** * @brief Gets the maximum number of tiles that can be added to the data @@ -67,11 +70,6 @@ class FVoxelDataTextures { */ UTexture* getTexture(const FString& attributeId) const; - /** - * @brief Retrieves how many data textures exist. - */ - int32 getTextureCount() const { return this->_propertyMap.Num(); } - /** * @brief Whether or not all slots in the textures are occupied. */ @@ -95,19 +93,25 @@ class FVoxelDataTextures { */ bool isSlotLoaded(int64 index) const; + /** + * @brief Checks the progress of slots with data being loaded into the + * megatexture. Retusn true if any slots completed loading. + */ + bool pollLoadingSlots(); + /** * @brief Whether the textures can be destroyed. Returns false if there are * any render thread commands in flight. */ bool canBeDestroyed() const; +private: /** - * @brief Checks the progress of slots with data being loaded into the - * megatexture. Retusn true if any slots completed loading. + * Value constants taken from CesiumJS. */ - bool pollLoadingSlots(); + static const uint32 MaximumTextureMemoryBytes = 512 * 1024 * 1024; + static const uint32 DefaultTextureMemoryBytes = 128 * 1024 * 1024; -private: /** * @brief Represents a slot in the voxel data texture that contains a single * tile's data. Slots function like nodes in a linked list in order to track @@ -174,7 +178,7 @@ class FVoxelDataTextures { Slot* _pEmptySlotsHead; Slot* _pOccupiedSlotsHead; - glm::uvec3 _dataDimensions; + glm::uvec3 _slotDimensions; glm::uvec3 _tileCountAlongAxes; uint32 _maximumTileCount; diff --git a/extern/cesium-native b/extern/cesium-native index 0050ee440..52b04c6e6 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 0050ee440c5590c664347cd39fda1a30192942fe +Subproject commit 52b04c6e6c9438bf0ae7b2c0136aadfbaeeef0e3 From cf8701ac8903690ab2438ffb1c7adcb98e92ca88 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 3 Jul 2025 16:42:19 -0400 Subject: [PATCH 25/34] Formatting and minor tweaks --- .../Private/CesiumGltfVoxelComponent.cpp | 4 +--- .../CesiumRuntime/Private/VoxelMegatextures.cpp | 16 +++++++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp index b9816367f..4f4ec1d85 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp @@ -9,6 +9,4 @@ UCesiumGltfVoxelComponent::UCesiumGltfVoxelComponent() { UCesiumGltfVoxelComponent::~UCesiumGltfVoxelComponent() {} -void UCesiumGltfVoxelComponent::BeginDestroy() { - Super::BeginDestroy(); -} +void UCesiumGltfVoxelComponent::BeginDestroy() { Super::BeginDestroy(); } diff --git a/Source/CesiumRuntime/Private/VoxelMegatextures.cpp b/Source/CesiumRuntime/Private/VoxelMegatextures.cpp index 1c47dd478..fb59b34b3 100644 --- a/Source/CesiumRuntime/Private/VoxelMegatextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelMegatextures.cpp @@ -92,11 +92,9 @@ FVoxelMegatextures::FVoxelMegatextures( // Find a best fit for the requested memory. Given a target volume // (maximumTexelCount) and the slot dimensions (xyz), find some scalar that - // fits the dimensions as close as possible. - float scalar = std::cbrtf(float(maximumTexelCount) / float(texelsPerSlot)); - glm::vec3 textureDimensions = glm::round(scalar * glm::vec3(slotDimensions)); - - this->_tileCountAlongAxes = glm::uvec3(textureDimensions) / slotDimensions; + // scales the slot dimensions to the volume as closely as possible. + float dimension = std::cbrtf(float(maximumTexelCount) / float(texelsPerSlot)); + this->_tileCountAlongAxes = glm::uvec3(glm::ceil(dimension)); if (glm::any(glm::equal(this->_tileCountAlongAxes, glm::uvec3(0)))) { UE_LOG( @@ -107,7 +105,7 @@ FVoxelMegatextures::FVoxelMegatextures( return; } - glm::uvec3 actualDimensions = this->_tileCountAlongAxes * slotDimensions; + glm::uvec3 textureDimensions = this->_tileCountAlongAxes * slotDimensions; this->_maximumTileCount = this->_tileCountAlongAxes.x * this->_tileCountAlongAxes.y * @@ -128,9 +126,9 @@ FVoxelMegatextures::FVoxelMegatextures( for (auto& propertyIt : this->_propertyMap) { FTextureResource* pResource = FCesiumTextureResource::CreateEmpty( TextureGroup::TEXTUREGROUP_8BitData, - actualDimensions.x, - actualDimensions.y, - actualDimensions.z, + textureDimensions.x, + textureDimensions.y, + textureDimensions.z, propertyIt.Value.encodedFormat.format, TextureFilter::TF_Nearest, TextureAddress::TA_Clamp, From b991b71ad4a322aaacc9b3876ed3cb5d0c22f3d0 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 9 Jul 2025 12:02:07 -0400 Subject: [PATCH 26/34] Refactor shaders --- Content/Materials/CesiumVoxelTemplate.hlsl | 822 ------------------ .../Materials/Instances/MI_CesiumVoxel.uasset | Bin 11778 -> 11858 bytes .../Materials/Layers/ML_CesiumVoxel.uasset | Bin 57129 -> 37165 bytes Shaders/Private/CesiumBox.usf | 68 ++ Shaders/Private/CesiumRayIntersection.usf | 103 +++ Shaders/Private/CesiumShaderConstants.usf | 10 + Shaders/Private/CesiumShape.usf | 98 +++ Shaders/Private/CesiumShapeConstants.usf | 5 + Shaders/Private/CesiumVectorUtility.usf | 36 + Shaders/Private/CesiumVoxelOctree.usf | 307 +++++++ Shaders/Private/CesiumVoxelTemplate.usf | 221 +++++ .../Private/CesiumVoxelMetadataComponent.cpp | 23 +- 12 files changed, 860 insertions(+), 833 deletions(-) delete mode 100644 Content/Materials/CesiumVoxelTemplate.hlsl create mode 100644 Shaders/Private/CesiumBox.usf create mode 100644 Shaders/Private/CesiumRayIntersection.usf create mode 100644 Shaders/Private/CesiumShaderConstants.usf create mode 100644 Shaders/Private/CesiumShape.usf create mode 100644 Shaders/Private/CesiumShapeConstants.usf create mode 100644 Shaders/Private/CesiumVectorUtility.usf create mode 100644 Shaders/Private/CesiumVoxelOctree.usf create mode 100644 Shaders/Private/CesiumVoxelTemplate.usf diff --git a/Content/Materials/CesiumVoxelTemplate.hlsl b/Content/Materials/CesiumVoxelTemplate.hlsl deleted file mode 100644 index 63acd8680..000000000 --- a/Content/Materials/CesiumVoxelTemplate.hlsl +++ /dev/null @@ -1,822 +0,0 @@ -// Copyright 2020-2024 CesiumGS, Inc. and Contributors - -/*============================================================================= - CesiumVoxelTemplate.hlsl: Template for creating custom shaders to style voxel data. -=============================================================================*/ - -/*======================= - BEGIN CUSTOM SHADER -=========================*/ - -struct CustomShaderProperties -{ -%s -}; - -struct CustomShader -{ -%s - - float4 Shade(CustomShaderProperties Properties) - { -%s - } -}; - -/*======================= - END CUSTOM SHADER -=========================*/ - -/*=========================== - BEGIN RAY + INTERSECTION UTILITY -=============================*/ - -#define CZM_INFINITY 5906376272000.0 // Distance from the Sun to Pluto in meters. -#define NO_HIT -CZM_INFINITY -#define INF_HIT (CZM_INFINITY * 0.5) - -#define CZM_PI_OVER_TWO 1.5707963267948966 -#define CZM_PI 3.141592653589793 -#define CZM_TWO_PI 6.283185307179586 - -struct Ray -{ - float3 Origin; - float3 Direction; -}; - -struct Intersection -{ - float t; - float3 Normal; -}; - -// Represents where a ray enters and leaves a volume. -struct RayIntersections -{ - Intersection Entry; - Intersection Exit; -}; - -struct RayIntersectionUtility -{ - float MaxComponent(float3 v) - { - return max(max(v.x, v.y), v.z); - } - - float MinComponent(float3 v) - { - return min(min(v.x, v.y), v.z); - } - - bool3 Equal(float3 v1, float3 v2) - { - return bool3(v1.x == v2.x, v1.y == v2.y, v1.z == v2.z); - } - - Intersection NewMissedIntersection() - { - Intersection result = (Intersection) 0; - result.t = NO_HIT; - return result; - } - - Intersection NewIntersection(float t, float3 Normal) - { - Intersection result = (Intersection) 0; - result.t = t; - result.Normal = Normal; - return result; - } - - RayIntersections NewRayIntersections(Intersection Entry, Intersection Exit) - { - RayIntersections result = (RayIntersections) 0; - result.Entry = Entry; - result.Exit = Exit; - return result; - } - - Intersection Min(in Intersection A, in Intersection B) - { - if (A.t <= B.t) - { - return A; - } - return B; - } - - Intersection Max(in Intersection A, in Intersection B) - { - if (A.t >= B.t) - { - return A; - } - return B; - } - - bool IsInRange(in float3 v, in float3 min, in float3 max) - { - bool3 inRange = (clamp(v, min, max) == v); - return inRange.x && inRange.y && inRange.z; - } - - /** - * Construct an integer value (little endian) from two normalized uint8 values. - */ - int ConstructInt(in float2 value) - { - return int(value.x * 255.0) + (256 * int(value.y * 255.0)); - } - - Intersection Multiply(in Intersection intersection, in float scalar) - { - return NewIntersection(intersection.t * scalar, intersection.Normal * scalar); - } - - /* - * Resolves two intersection ranges of the ray by computing their overlap. - * For example: - * A |---------| - * B |-------------| - * Ray: O =========================> - * Output: |---| - */ - RayIntersections ResolveIntersections(in RayIntersections A, in RayIntersections B) - { - bool missed = (A.Entry.t == NO_HIT) || (B.Entry.t == NO_HIT) || - (A.Exit.t < B.Entry.t) || (A.Entry.t > B.Exit.t); - - if (missed) - { - Intersection miss = NewMissedIntersection(); - return NewRayIntersections(miss, miss); - } - - Intersection entry = Max(A.Entry, B.Entry); - Intersection exit = Min(A.Exit, B.Exit); - - return NewRayIntersections(entry, exit); - } -}; - -/*=========================== - END RAY + INTERSECTION UTILITY -=============================*/ - -/*=========================== - BEGIN SHAPE UTILITY -=============================*/ - -#define BOX 1 -#define CYLINDER 2 -#define ELLIPSOID 3 - -struct ShapeUtility -{ - RayIntersectionUtility Utils; - - int ShapeConstant; - - float3 MinBounds; - float3 MaxBounds; - - /** - * Converts a position from Unit Shape Space coordinates to UV Space. - * [-1, -1] => [0, 1] - */ - float3 UnitToUV(float3 UnitPosition) - { - return 0.5 * UnitPosition + 0.5; - } - - /** - * Converts a position from UV Space coordinates to Unit Shape Space. - * [0, 1] => [-1, -1] - */ - float3 UVToUnit(float3 UVPosition) - { - return 2.0 * UVPosition - 1.0; - } - - /** - * Interpret the input bounds (Local Space) according to the voxel grid shape. - */ - void Initialize(in int InShapeConstant) - { - ShapeConstant = InShapeConstant; - - if (ShapeConstant == BOX) - { - // Default unit box bounds. - MinBounds = -1; - MaxBounds = 1; - } - } - - /** - * Tests whether the input ray (Unit Space) intersects the box. Outputs the intersections in Unit Space. - */ - RayIntersections IntersectBox(in Ray R) - { - // Consider the box as the intersection of the space between 3 pairs of parallel planes. - - // Compute the distance along the ray to each plane. - float3 t0 = (MinBounds - R.Origin) / R.Direction; - float3 t1 = (MaxBounds - R.Origin) / R.Direction; - - // Identify candidate entries/exits based on distance from ray position. - float3 entries = min(t0, t1); - float3 exits = max(t0, t1); - - // The actual intersection points are the furthest entry and the closest exit. - // Do not allow intersections to go behind the shape (negative t). - RayIntersections result = (RayIntersections) 0; - float entryT = max(Utils.MaxComponent(entries), 0); - float exitT = max(Utils.MinComponent(exits), 0); - - if (entryT > exitT) - { - Intersection miss = Utils.NewMissedIntersection(); - return Utils.NewRayIntersections(miss, miss); - } - - // Compute normals - float3 directions = sign(R.Direction); - bool3 isLastEntry = bool3(Utils.Equal(entries, float3(entryT, entryT, entryT))); - result.Entry.Normal = -1.0 * float3(isLastEntry) * directions; - result.Entry.t = entryT; - - bool3 isFirstExit = bool3(Utils.Equal(exits, float3(exitT, exitT, exitT))); - result.Exit.Normal = float3(isFirstExit) * directions; - result.Exit.t = exitT; - - return result; - } - - /** - * Tests whether the input ray (Unit Space) intersects the shape. - */ - RayIntersections IntersectShape(in Ray R) - { - RayIntersections result; - - [branch] - switch (ShapeConstant) - { - case BOX: - result = IntersectBox(R); - break; - default: - return Utils.NewRayIntersections(Utils.NewMissedIntersection(), Utils.NewMissedIntersection()); - } - - // Set start to 0.0 when ray is inside the shape. - result.Entry.t = max(result.Entry.t, 0.0); - - return result; - } - - /** - * Scales the input UV coordinates from [0, 1] to their values in UV Shape Space. - */ - float3 ScaleUVToShapeUVSpace(in float3 UV) - { - return UV; - } - - /** - * Converts the input position (vanilla UV Space) to its Shape UV Space relative to the - * box geometry. Also outputs the Jacobian transpose for future use. - */ - float3 ConvertUVToShapeUVSpaceBox(in float3 PositionUV, out float3x3 JacobianT) - { - // For Box, Cartesian UV space = UV shape space, so we can use PositionUV as-is. - // The Jacobian is the identity matrix, except that a step of 1 only spans half the shape - // space [-1, 1], so the identity is scaled. - JacobianT = float3x3(0.5f, 0, 0, 0, 0.5f, 0, 0, 0, 0.5f); - return PositionUV; - } - - /** - * Converts the input position (vanilla UV Space) to its Shape UV Space relative to the - * voxel grid geometry. Also outputs the Jacobian transpose for future use. - */ - float3 ConvertUVToShapeUVSpace(in float3 UVPosition, out float3x3 JacobianT) - { - switch (ShapeConstant) - { - case BOX: - return ConvertUVToShapeUVSpaceBox(UVPosition, JacobianT); - default: - // Default return - JacobianT = float3x3(1, 0, 0, 0, 1, 0, 0, 0, 1); - return UVPosition; - } - } -}; - -/*=========================== - END SHAPE UTILITY -=============================*/ - -/*=========================== - BEGIN OCTREE TRAVERSAL UTILITY -=============================*/ - -// An octree node is encoded by 9 texels. -// The first texel contains the index of the node's parent. -// The other texels contain the indices of the node's children. -#define TEXELS_PER_NODE 9 - -#define OCTREE_MAX_LEVELS 32 // Hardcoded because HLSL doesn't like variable length loops - -#define OCTREE_FLAG_EMPTY 0 -#define OCTREE_FLAG_LEAF 1 -#define OCTREE_FLAG_INTERNAL 2 - -#define MINIMUM_STEP_SCALAR (0.02) -#define SHIFT_EPSILON (0.001) - -struct Node -{ - int Flag; - // "Data" is interpreted differently depending on the flag of the node. - // For internal nodes, this refers to the index of the node in the octree texture, - // where its children data are stored. - // For leaf nodes, this represents the index in the megatexture where the node's - // actual metadata is stored. - int Data; - // Only applicable to leaf nodes. - int LevelDifference; -}; - -/* - * Representation of an in-progress traversal of an octree. Points to a node at the given coords, - * and keeps track of the parent in case the traversal needs to be reversed. - * - * NOTE the difference between "coordinates" and "index". - * Octree coordinates are given as int4, representing the X, Y, Z, of the node in its tree level (W). - * These are not to be confused with the index of a node within the actual texture. - */ -struct OctreeTraversal -{ - int4 Coords; // W = Octree Level (starting from 0); XYZ = Index of node within that level - int Index; -}; - -struct TileSample -{ - int Index; // Index of the sample in the voxel data texture. - int4 Coords; - float3 LocalUV; -}; - -struct Octree -{ - Texture2D NodeData; - uint TextureWidth; - uint TextureHeight; - uint TilesPerRow; - uint3 GridDimensions; - - ShapeUtility ShapeUtils; - - /** - * Sets the octree texture and computes related variables. - */ - void SetNodeData(in Texture2D DataTexture) - { - NodeData = DataTexture; - NodeData.GetDimensions(TextureWidth, TextureHeight); - TilesPerRow = TextureWidth / TEXELS_PER_NODE; - } - - /** - * Given an octree-relative UV position, converts it to the corresponding position - * within the UV-space of the *tile* at the specified coordinates. - */ - float3 GetTileUV(in float3 PositionUV, in int4 Coords) - { - // Get the size of the tile at the given level. - float tileSize = float(1u << Coords.w); - return PositionUV * tileSize - float3(Coords.xyz); - } - - /** - * Given an octree-relative UV position, checks whether it is inside the tile - * specified by the octree coords. Assumes the position is always inside the - * root tile of the tileset. - */ - bool IsInsideTile(in float3 PositionUV, in int4 OctreeCoords) - { - float3 tileUV = GetTileUV(PositionUV, OctreeCoords); - bool isInside = ShapeUtils.Utils.IsInRange(tileUV, 0, 1); - return isInside || OctreeCoords.w == 0; - } - - /** - * Gets the size of a voxel cell within the given octree level. - */ - float3 GetVoxelSizeAtLevel(in int Level) - { - float3 sampleCount = float(1u << Level) * float3(GridDimensions); - float3 voxelSizeUV = 1.0 / sampleCount; - return ShapeUtils.ScaleUVToShapeUVSpace(voxelSizeUV); - } - - TileSample GetSampleFromNode(in Node Node, in float4 OctreeCoords) - { - TileSample result = (TileSample) 0; - result.Index = (Node.Flag != OCTREE_FLAG_EMPTY) - ? Node.Data - : -1; - float denominator = float(1u << Node.LevelDifference); - result.Coords = int4(OctreeCoords.xyz / denominator, OctreeCoords.w - Node.LevelDifference); - return result; - } - - Node GetNodeFromTexture(in int2 TextureIndex) - { - float4 nodeData = NodeData.Load(int3(TextureIndex.x, TextureIndex.y, 0)); - Node node = (Node) 0; - node.Flag = int(nodeData.x * 255.0); - node.LevelDifference = int(nodeData.y * 255.0); - node.Data = ShapeUtils.Utils.ConstructInt(nodeData.zw); - return node; - } - - /** - * Given the index of a node, converts it to texture indices, which are needed to actually - * retrieve the node data from the octree texture. - */ - int2 GetTextureIndexOfNode(in int NodeIndex) - { - int2 result; - result.x = (NodeIndex % TilesPerRow) * TEXELS_PER_NODE; - result.y = NodeIndex / TilesPerRow; - return result; - } - - Node GetChildNode(in int NodeIndex, in int3 ChildCoord) - { - int childIndex = ChildCoord.z * 4 + ChildCoord.y * 2 + ChildCoord.x; - int2 textureIndex = GetTextureIndexOfNode(NodeIndex); - textureIndex.x += 1 + childIndex; - return GetNodeFromTexture(textureIndex); - } - - Node GetParentNode(in int NodeIndex) - { - return GetNodeFromTexture(GetTextureIndexOfNode(NodeIndex)); - } - - /** - * Given a UV position [0, 1] within the voxel grid, traverse to the leaf containing it. - * Outputs the node's corresponding data texture index and octree coordinates. - */ - Node TraverseToLeaf(in float3 PositionUV, inout OctreeTraversal Traversal) - { - // Get the size of the node at the current level - float sizeAtLevel = exp2(-1.0 * float(Traversal.Coords.w)); - float3 start = float3(Traversal.Coords.xyz) * sizeAtLevel; - float3 end = start + sizeAtLevel; - - Node child; - - for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) - { - // Find the octree child that contains the given position. - // Example: the point (0.75, 0.25, 0.75) belongs to the child at coords (1, 0, 1) - float3 center = 0.5 * (start + end); - int3 childCoord = step(center, PositionUV); - - // Get the octree coordinates for the next level down. - Traversal.Coords = int4(Traversal.Coords.xyz * 2 + childCoord, Traversal.Coords.w + 1); - - child = GetChildNode(Traversal.Index, childCoord); - - if (child.Flag != OCTREE_FLAG_INTERNAL) - { - // Found leaf - stop traversing - break; - } - - // Keep going! - start = lerp(start, center, childCoord); - end = lerp(center, end, childCoord); - Traversal.Index = child.Data; - } - - return child; - } - - void BeginTraversal(in float3 PositionUV, out OctreeTraversal Traversal, out TileSample Sample) - { - Traversal = (OctreeTraversal) 0; - - // Start from root - Node currentNode = GetNodeFromTexture(int2(0, 0)); - if (currentNode.Flag == OCTREE_FLAG_INTERNAL) - { - currentNode = TraverseToLeaf(PositionUV, Traversal); - } - - Sample = GetSampleFromNode(currentNode, Traversal.Coords); - Sample.LocalUV = clamp(GetTileUV(PositionUV, Sample.Coords), 0.0, 1.0); - } - - void ResumeTraversal(in float3 PositionUV, inout OctreeTraversal Traversal, inout TileSample Sample) - { - if (IsInsideTile(PositionUV, Traversal.Coords)) - { - // Continue to sample the same tile, marching to a different voxel. - Sample.LocalUV = clamp(GetTileUV(PositionUV, Sample.Coords), 0.0, 1.0); - return; - } - - // Otherwise, go up tree until we find a parent tile containing the position. - for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) - { - Traversal.Coords.w -= 1; // Up one level - Traversal.Coords.xyz /= 2; // Get coordinates of the parent tile. - - if (IsInsideTile(PositionUV, Traversal.Coords)) - { - break; - } - Node parent = GetParentNode(Traversal.Index); - Traversal.Index = parent.Data; - } - - // Go down tree - Node node = TraverseToLeaf(PositionUV, Traversal); - Sample = GetSampleFromNode(node, Traversal.Coords); - Sample.LocalUV = clamp(GetTileUV(PositionUV, Sample.Coords), 0.0, 1.0); - } - - /** - * Given UV coordinates within a tile, and the voxel dimensions along a ray - * passing through the coordinates, find the intersections where the ray enters - * and exits the voxel cell. - * - * Outputs the distance to the points and the surface normals in UV Shape Space. - */ - RayIntersections GetVoxelIntersections(in float3 TileUV, in float3 VoxelSizeAlongRay) - { - float3 voxelCoord = TileUV * float3(GridDimensions); - float3 directions = sign(VoxelSizeAlongRay); - float3 positiveDirections = max(directions, 0.0); - float3 entryCoord = lerp(ceil(voxelCoord), floor(voxelCoord), positiveDirections); - float3 exitCoord = entryCoord + directions; - - RayIntersections Intersections; - - float3 distanceFromEntry = -abs((entryCoord - voxelCoord) * VoxelSizeAlongRay); - float lastEntry = ShapeUtils.Utils.MaxComponent(distanceFromEntry); - bool3 isLastEntry = ShapeUtils.Utils.Equal(distanceFromEntry, float3(lastEntry, lastEntry, lastEntry)); - Intersections.Entry.Normal = -1.0 * float3(isLastEntry) * directions; - Intersections.Entry.t = lastEntry; - - float3 distanceToExit = abs((exitCoord - voxelCoord) * VoxelSizeAlongRay); - float firstExit = ShapeUtils.Utils.MinComponent(distanceToExit); - bool3 isFirstExit = ShapeUtils.Utils.Equal(distanceToExit, float3(firstExit, firstExit, firstExit)); - Intersections.Exit.Normal = float3(isFirstExit) * directions; - Intersections.Exit.t = firstExit; - - return Intersections; - } - - /** - * Computes the next intersection in the octree by stepping to the next voxel cell - * within the shape along the ray. - * - * Outputs the distance relative to the initial intersection, as opposed to the distance from the - * origin of the ray. - */ - Intersection GetNextVoxelIntersection(in TileSample Sample, in float3 Direction, in RayIntersections ShapeIntersections, in float3x3 JacobianT, in float CurrentT) - { - // The Jacobian is computed in a space where the shape spans [-1, 1]. - // But the ray is marched in a space where the shape fills [0, 1]. - // So we need to scale the Jacobian by 2. - float3 gradient = 2.0 * mul(Direction, JacobianT); - float3 voxelSizeAlongRay = GetVoxelSizeAtLevel(Sample.Coords.w) / gradient; - RayIntersections voxelIntersections = GetVoxelIntersections(Sample.LocalUV, voxelSizeAlongRay); - - // Transform normal from UV Shape Space to Cartesian space. - float3 voxelNormal = normalize(mul(JacobianT, voxelIntersections.Entry.Normal)); - // Then, compare with the original shape intersection to choose the appropriate normal. - Intersection voxelEntry = ShapeUtils.Utils.NewIntersection(CurrentT + voxelIntersections.Entry.t, voxelNormal); - Intersection entry = ShapeUtils.Utils.Max(ShapeIntersections.Entry, voxelEntry); - - float fixedStep = ShapeUtils.Utils.MinComponent(abs(voxelSizeAlongRay)); - float shift = fixedStep * SHIFT_EPSILON; - float dt = voxelIntersections.Exit.t + shift; - if ((CurrentT + dt) > ShapeIntersections.Exit.t) - { - // Stop at end of shape. - dt = ShapeIntersections.Exit.t - CurrentT + shift; - } - float stepSize = clamp(dt, fixedStep * MINIMUM_STEP_SCALAR, fixedStep + shift); - - return ShapeUtils.Utils.NewIntersection(stepSize, entry.Normal); - } -}; - -/*=========================== - END OCTREE TRAVERSAL UTILITY -=============================*/ - -/*=========================== - BEGIN VOXEL DATA TEXTURE UTILITY -=============================*/ - -struct VoxelDataTextures -{ - %s - int ShapeConstant; - uint3 TileCount; // Number of tiles in the texture, in three dimensions. - - // NOTE: Unlike Octree, these dimensions are specified with respect to the voxel attributes - // in the glTF model. For box and cylinder voxels, the YZ dimensions will be swapped. - uint3 GridDimensions; - uint3 PaddingBefore; - uint3 PaddingAfter; - - int3 TileIndexToCoords(in int Index) - { - if (TileCount.x == 0 || TileCount.y == 0 || TileCount.z == 0) - { - return 0; - } - int ZSlice = TileCount.x * TileCount.y; - int Z = Index / ZSlice; - int Y = (Index % ZSlice) / TileCount.x; - int X = Index % TileCount.x; - return int3(X, Y, Z) * (GridDimensions + PaddingBefore + PaddingAfter); - } - - CustomShaderProperties GetProperties(in TileSample Sample) - { - // Compute the tile location within the texture. - float3 TileCoords = TileIndexToCoords(Sample.Index); - - // Compute int coordinates of the voxel within the tile. - float3 LocalUV = Sample.LocalUV; - uint3 DataDimensions = GridDimensions + PaddingBefore + PaddingAfter; - - 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); - } - - 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 - - return Properties; - } -}; - -/*=========================== - END VOXEL DATA TEXTURE UTILITY -=============================*/ - -/*=========================== - MAIN FUNCTION BODY -=============================*/ - -#define STEP_COUNT_MAX 1000 -#define ALPHA_ACCUMULATION_MAX 0.98 // Must be > 0.0 and <= 1.0 - -Octree VoxelOctree; -VoxelOctree.ShapeUtils = (ShapeUtility) 0; -VoxelOctree.ShapeUtils.Initialize(ShapeConstant); - -Ray R = (Ray) 0; -R.Origin = RayOrigin; -R.Direction = RayDirection; - -// Input ray is Unit Space. -// -// Ultimately, we want to traverse the voxel grid in a [0, 1] UV space to simplify much of -// the voxel octree math. This is simply referred to UV space, or "vanilla" UV space. -// However, this UV space won't always be a perfect voxel cube. It must conform to the specified -// shape of the voxel volume. Voxel cells may be curved (e.g., around a cylinder), or non-uniformly -// scaled. This shape-relative UV space is aptly referred to as Shape UV Space. -// -// Shape UV Space is DIFFERENT from the shape's Unit Space. Think of Unit Space as the perfect -// version of the grid geometry: a solid box, cylinder, or ellipsoid, within [-1, 1]. -// The actual voxel volume can be a subsection of that perfect unit space, e.g., a hollowed -// cylinder with radius [0.5, 1]. -// -// 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, -// 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. -// Spaces will be referred to as follows: -// -// Unit Space: Unit space of the grid geometry from [-1, 1]. A perfectly solid box, cylinder, or ellipsoid. -// Shape UV Space: Voxel space from [0, 1] conforming to the actual voxel volume. The volume could be a box, a part of a cylinder, or a region on an ellipsoid. -// (Vanilla) UV Space: Voxel space within an untransformed voxel octree. This can be envisioned as a simple cube spanning [0, 1] in three dimensions. - -RayIntersections Intersections = VoxelOctree.ShapeUtils.IntersectShape(R); -if (Intersections.Entry.t == NO_HIT) { - return 0; -} - -// Intersections are returned in Unit Space. Transform to UV space [0, 1] for raymarching through octree. -R.Origin = VoxelOctree.ShapeUtils.UnitToUV(R.Origin); -R.Direction = R.Direction * 0.5; - -// Initialize octree -VoxelOctree.SetNodeData(OctreeData); -VoxelOctree.GridDimensions = GridDimensions; - -// Initialize data textures -VoxelDataTextures DataTextures; -DataTextures.ShapeConstant = ShapeConstant; -DataTextures.TileCount = TileCount; - -// Account for y-up -> z-up conventions for certain shapes. -switch (ShapeConstant) { - case BOX: - DataTextures.GridDimensions = round(GridDimensions.xzy); - DataTextures.PaddingBefore = round(PaddingBefore.xzy); - DataTextures.PaddingAfter = round(PaddingAfter.xzy); - break; - default: - DataTextures.GridDimensions = round(GridDimensions); - DataTextures.PaddingBefore = round(PaddingBefore); - DataTextures.PaddingAfter = round(PaddingAfter); - break; -} - -%s - -float CurrentT = Intersections.Entry.t; -float EndT = Intersections.Exit.t; -float3 PositionUV = R.Origin + CurrentT * R.Direction; - -// The Jacobian is necessary to compute voxel intersections with respect to the grid's shape. -float3x3 JacobianT; -float3 PositionShapeUVSpace = VoxelOctree.ShapeUtils.ConvertUVToShapeUVSpace(PositionUV, JacobianT); - -float3 RawDirection = R.Direction; - -OctreeTraversal Traversal; -TileSample Sample; -VoxelOctree.BeginTraversal(PositionShapeUVSpace, Traversal, Sample); -Intersection NextIntersection = VoxelOctree.GetNextVoxelIntersection(Sample, RawDirection, Intersections, JacobianT, CurrentT); - -float4 AccumulatedResult = float4(0, 0, 0, 0); - -CustomShaderProperties Properties; -CustomShader CS; - -for (int step = 0; step < STEP_COUNT_MAX; step++) { - if (Sample.Index >= 0) { - Properties = DataTextures.GetProperties(Sample); - // TODO: expose additional properties? - float4 result = CS.Shade(Properties); - AccumulatedResult += result; - - // Stop traversing if the alpha has been fully saturated. - if (AccumulatedResult.w > ALPHA_ACCUMULATION_MAX) { - AccumulatedResult.w = ALPHA_ACCUMULATION_MAX; - break; - } - } - - if (NextIntersection.t <= 0.0) { - // Shape is infinitely thin. The ray may have hit the edge of a - // foreground voxel. Step ahead slightly to check for more voxels - NextIntersection.t = 0.00001; - } - - // Keep raymarching - CurrentT += NextIntersection.t; - if (CurrentT > EndT) { - break; - } - - PositionUV = R.Origin + CurrentT * R.Direction; - PositionShapeUVSpace = VoxelOctree.ShapeUtils.ConvertUVToShapeUVSpace(PositionUV, JacobianT); - VoxelOctree.ResumeTraversal(PositionShapeUVSpace, Traversal, Sample); - NextIntersection = VoxelOctree.GetNextVoxelIntersection(Sample, RawDirection, Intersections, JacobianT, CurrentT); -} - -// Convert the alpha from [0,ALPHA_ACCUMULATION_MAX] to [0,1] -AccumulatedResult.a /= ALPHA_ACCUMULATION_MAX; - -return AccumulatedResult; diff --git a/Content/Materials/Instances/MI_CesiumVoxel.uasset b/Content/Materials/Instances/MI_CesiumVoxel.uasset index b6ca70936bf9fb27b8c9c2b041f8f0c1f776ccaf..a36598ac242c9e920b33633d5981fb2699401418 100644 GIT binary patch delta 3059 zcma)8eQZ=k5TAFwD|aolceM9jDN@Sypw;W0m42ZHDwfg;0-_WWY+{>GZB0dEH3Amj zl`4u~79UQfY{K&g`4LZtq?zb9pRFaw@dgXh_9t!v_Ii^xOS|@2uVO%kNt^ z9Ire1;m1#;25XJRRLb%Mqp9O-N)97t!T>-=SQQel#cOSxL2n4ahddnyKoN?|0DiIn z$aV#J2A{DAz{qIHeHTxN4WE2=WTGn2y87Sx$&dPcOFECfy?Ec}XNKP*JjJ19eBj03 z^6#rW3vm{Ub$2w!?(QGz?;GxqHz%5;9bm`CrYXZ6fL@Z8jSK`yIE@KZB^;d^u@ISH z6krc3A_?)UakFSCZA@2te()F-;va9N4^F+_U7!Bjn=DMfSA5P`ScJ%JgG2r84?n*9 zF?jXt!1d`v3*I%-gQXXZKnKU|5EJ1tV^Mk_bkdLoB7KsLf8v@p%gTYKw0WYW90C47 z+_Nmtg_}U!GqdCDY(N51lZoaEcnNVYIj|WxYC6#YhrwTn8)aFKr*LD^hFiUiz^CQ_ zKFzi(h zg8N+-R=zK@Z4zs{$OV_lh;!i*lainm0{E!qf>AMURf>aFov~IdH7)Zw_Af>ro##xt z9nk$|wfMmDd&=jDq-mOm7)jx_b1HjdDx7DtM_e$?@QaKh!0jx5EsCvMDU=ZqZI)%y zL8Qd|_$**dD4`-DXxv(nK5LyQDlDi#b5tZFo5cK@6<)lGKwR*I1BEE8?;9HK7hgq| zh?O_0#+=YnV+Wu?9w{G5o86?V>m z<|IWGRpx;j-ElR8bIWjy^mKHIa(faq9etj`4n8`IB}m^!BCTRj<*4L=QOAf>GEw?| z6LmQZ+-lLLw)UU{vS;jVkUewvCO>9t*r*rdwpkpSN#DXo{hiR?DoXkuxfS|bqY|Tw zhbXUv3b4(wgC17TNxs{K z3i|=+GSg{*b-Ar8r!?GJ*EWm8d5X}=M!hQ8hR{DT+ORB%;dUeMW_M!k;(V-g;||ui z21Hvn9^B-D%&U&=v=^cJTR@&GUEFI}N7!nXbL*tuGBVGnfdI=$K&o`u^dX_<dk)M3r#9BBGetrWD}9pXs5-UA)tOuX$sKv*Di delta 2981 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 literal 57129 zcmdtL30zdw_W=F?MJ34%_tYnD1F|pThBCmg$iT1r`(Yh*?) zxRq^cW{X+5RA0*!+cUpvn%bh~(qj5Q=PqyF3^NF1|Nr;@dmo1P-d)Z;_uO;OJ@?#m z@7w#Z@0#Cl-MTf^O%Oa>1>q9h@p6aXbC3Pr`9@Odg7#lMSm*Qh$l-wycIvTXK@UZr zUq11bQ+3l`zVM|BgpJ;Nf5cx2r`<2k6L!p=;XAk$gv~uROSe^-cIKyyq%De_ufJ0U zVPh>fKCOyB}>g>pYDSo?B}AFPoIlkcIB?GgV!qGj0;de*q{hO2!?xLjMl6x zEsisqvJED!Mjx1>v1m;?jouuXtf|zR%z-J%Q)rqTV}(`^Sp;Dz{Juu4;5yM!5cy z{~?Qm#uSvoyT!35uxw?1zRsdE8Z`Q}QcFpx#Vow#^VY9-V~ok9sZ2K+OSC3SrI7gY zDIG~~v6ytZr50_L)}pQ~(VB&x!zXM*n9)Mdaa+>v!Y8PP7-Q5MO~S9^o}bzcf^uVw z#l>2KCAu^>SFg>{nRRdzG6s$w-2r2hHD*h?Nn57VmTU8c)Tm)IJrR!4sFx7v*y`o# zu9!Bf$XHHsP1O`@(+uf4gU~I&`$#X0%rFV{{FHS!sb3 ztWsxE{oid5*CwRJ3H?f2{ZlMZL>?uxV7l9!67|L0=iGv1o*q&wcX{R$j%+ zmufF`^DFoj`-h4{<4Lm3nnJBGC-smPYv4F~2^k8Ad;ErEl50z4<8_k4I)h3O> zTwpX6XBkUPd0N3(n|rITJYkMD4-k$q8cq39S|O*X|62oZmx@8$w4=|jNPm@~v{>Sl zj4z&zLhh51+o;z=QAka%VZTNYT1jmYpPd*hq<&xb2nZTk9@^^S&s$uvD_LQY@Wgzf z$6pVHcCv|KJTde>Uo#=QR055FJmxnBWMcDLp_zq>-cNtf2EqtShY3%sN|(38SH?4f z>GB&3d&mN;c`1fV=iTGg(_uWr=LciRXlx!1kz|{~n^4JRgjtwzc~LtgjZH$c(B)C{ z2qX;;reMuNz;mX*P?p3CCUMmsOc8I=<%_X8kR8b5W}&b9;5-C~;j&<{;`nsr`1n#? zz7UiCXAurpKpo2)e-*gnBX&~Z!RiI`Jop<*4`I?r5miVj5^om1_;qO;DFOCd;nb2@ zZz6dU4IrSH*w}D%SQ3UML-*GJtD&ItiZRC_UdhHhjXqXqVytm$*t4&rFeSm9hlMWZ zdk;ixQ#9u3!ivXEj6f9eMJW`vUvi1RMGNov&bgnz#S6!jDcks4rtrz76YWsgaH@+k zvgeOIsqG_+vol2Nom?*Tpu;)?i^bKK@k8a=G^NBSHVG{AfIrk#D(=~b1LDrGR*3HzQa6k}dC`&7Jwp1=a zR!G;tkZULul?9-*2>$JhKf#Ld&}c0PH?7cR`;Qlp0xX!Oeg2#4nKB40Y3lHv>u9J6 z$^w`K>*RquJh3Nb3cAJNJrGo;rqarn^R8SNgNzP=X(nAE456a;o(V_@lh#sdG6+En zE20svtRhVb4(0~4MPslCeI8%`88#>lPSI3|(Z*6kzFA0#{<~5Zp)>G^KYO-*81Pd=syyr~gX+nguIhPZW6a+g7XY-qX{3cZeP-Ym% zy12bz4b~Z0@X*yCW@2+^Szssy36j6zt>xI+vn(cw%|`7t4J27DrcxRA1qrKfAZMs` zdL&sX(5vsE0yEMXgJ?+2z9JtKZ0by9YF1oYX37-8F2b(6eC|S-D}|@0Wo4yhXU3?e zj5nI}`NAOI;!fCQqy#px+yC_2fYLxBFdfYZdse1RScz>T#Zlr12^(j0yyYMwSm>xP zUF9GmMEEf2nFyq?6%-1|Ky80Fq(?SL$sFWWdM%X26Aa+W&zRwCPf5= z1ZG=wdJq(W(J<4P4&xLZQ$fQk0XbvaH;P@qza*M&ye?aiVhQS+R%o}^3Lo =xPnx~J^==DNzaLV0- z8%<8sKc;1TD7@bVyF3SFt#>LYfEvASrdIgs&7yOThp0@g$q4flA^1N9+q@k@$%3I; zV@lUBxhYg8Wn|+#vtfBK+2Mh*QTDsXVs*V8r!UR5m^677Vc2VL>wFw0QEMwe&}!KL zCgJeM#Gg((uCvtTgqizZxF5Ayj*^uwQX1aY!skQB=p5Ica*eh2gNKwY5FJ9b>wX`+ zI9|?;yn*dz3MbFJ&;u27jwDaU->IJep{mPLR83Q~77ZDnPgiYUiao||2EX>MiYr8+ zv6oQK)+6YM#G-SK9h?i{3h zu!-r&)Ev^@4&Ll0!kg2?bX84EH@AuD9%^E`hntvgUK7*JZ(_PfnwajXr zn6A2s=^krhy2qQCZjpm@l`Krf?iYZcXF5p7`F;tD!&`Y1)A78H_+Ac=<04OY6Y$|h z4uS{lYiK!Z3%^u+fD5f!@{0wNAo(+Zl3?-lj^Lx7EQfC%&QsI79H#uasqk?wDu=&! zM9bm-EvpmK2W1T6gz(qH53dfR=$-$U<1Bx6guf&BoIV`>YySZL4Lkf%_Hq2jp{5Mi z_|f#v|I2ZfKRd$T5qysSkzxvQZ5)S8?{b*(XGie=vcn%`GUh@0C$h|z?G!HmFNZ0A zb_D-VJNQ)qgv0O2==1gt4w>HNFy+sV;QwI5Ae`n!y`N{cb&#^lTpUY1Uf8XbK7Cx7soIW3aacALk`N{F`{MDU>&*dkF z-{b2$3m**-SO$mR_nVf(|JAO4;hc{5uk{Siy0`a!!`DN&Kc+|Z8~YNzqgfhPRnS(| z34Um2!bM$NrIo`*I6ncR*herNVd6qEe@2Twe?}`Ze@4?8e@4R>f9@xH?k{^5WzXGZ z&))2r*O|*Q#0Tq*a1qA=@N3Q9d0NC3>A}m!y!g!H@6;X3M%rQ@MA~3E93P|);(%>~ z?Sk~jcEonYc1D^Z9@r*`FV+R?hBQMOVjE#wVO^1S*j^(1`oj;~4BHOrg>8ql!uGSd z5ID3SuG!rB^41BVV^UmVoZte%E?DrbZ-f}(uC{GG+P1mNqn(Fm`@1|l-P5^KhYp>* zd-n3W$7kSBU&X*7L;ORcBK(6QgN6(lo;)HlMwO75Fmza2c50kDDn3DlKwR2;dUop2 zsek9r{Z#=&0#wbuZoMmXYKu6zxp)h%om|{Hx!n2y>+I6ng`hKy7%bWYtY~!3LoFF@L>_dM~sY#RmH_8Bqn94vvbBzm^djfUt3UEq?=~8 zl$MoORL*>O-uy=%U9hnFsin)7uUNV2>6*3c)^DiWxM}m2S6|z{W9RF;-q`)#`ycGv zf8gMUA00Vbf9&%wzWnOz|DHT``iC=Te>`{o(&gW-T)lSv_dou`dSN~JMQdhPuTD@e z*H*3ETD8G?xww|Y&#hCd*8PLsJ1aBVXl8Wr4w>85D|*TEuk39%AT;x0*Id(Q9^D3p z{ddqMtQx6V(<=5*i)toS%v!Inh4yYP(0Fd0gt5Zq-HNqU9(ko3kM@31803`^)gg7+ z_Jv1QRC&amORal7Nzi(2Pj`K3(yZY4LpjgCwrh1kkAXAJZ%Gl9s-kW4H@$rF*e#*g z;tHeMuwvuQzouNOZ9i@3gx{yD`&~Xir_Gd0b%!&3L)X!h=$~^g0Rgdp<+s|A`jB1yfy8Ua@)!H^0DlcKtb=|USf7Lx5)f%ws zoRR$PtRnbxxw8MS{fiEKzFHVCxT>dWY|)ik$X8 zkDJ$Kr9s%X=KZ*xJ6x*&c(ubx&CwryCi|IlCaJ!=B?QOMyZP6}p)-%ZHzT0e`2zFb z^*#EXz5HT*pDpisjPFyO^R;UE&e2;Z>}@kbv+HKTOItd9y{x0p-nV_)XSW}ve(aav z|(c?js23Am%LZi<%a?8nHxpj1^joJJyI||h31Mp{ z_Pyfr@$rg)ZXa%ccHP+T=B%1MEB?adU!H&d{&R2W)_<#+B6Zw}cme|6)U+XXa0B zc8r>P$}pnti@Ni(yT4K&Gdyi@-e-sNhJ0JTZ9vvx!5;Q@#ntFioJx7u)Syg{k1l7 z_RoU9SLS@bsEns(xpx(D|P3#3cqQEkK+IS?cPq?T93<1 zcrbX2B3p2I*KNlHW%43(PR5Lc+_f|7-%K-pv_zG#wyKA(I{9yf-sRw#uTxM+O`LhN=|L?-CzITp4V#ItSh{b>5}n6#o4e|-7f4C+P`0W_W0^;qb_A+ zRtwL0edV2)@sz6l<3Z}jAM&aGz_K~};RkvNr@}vNGcW#)qsn)C29=Jz*yZn`ixzj< z+hfE_1^dkR54?Ete8SOhgx9BkQ@eO(zd19@JDpW8AGlcR8b^(P(*Qr2Y&Q{K+Z z>i%oik1mQ0>ucSA@_MtiznAb#>%nCged_n0FLH_La_`rr3+A<1?AHI5aA4Y+l|>zX zJ>B!>iJLp#>|DPyY~g3Kv#x9neyOW#zob|ColD67(O0|asST5aCAvS}Jru6n@aml9 zu6XN})5I^DH;a{koAJ}M#lOO;B zlejBmSGsHcio>(ko*1G!pSDA_E9dQH-aRhIfAap|rDeSWV{50nR1J51aY*FI?CdLB zGt~W8bl#XW*SEB;)sM$Uu0E#SoWA-}RF}{>9nMD8ijfDyPAs}w8|8Dd#^o1JMcBI) z8v`?vy{?{DxE`^1T}n>d?Q^oKua7D$^OubJYN4bq=`BN_-JhM=biMBBh4-{icN0E5 zxgp`r9V0_LF8W*%a;qmLO$|OeV8Dw?Z@&c}dOwoWLwus`*6`jh&4}uI?@PMEq&*eN z!66ZG2X>7;+jZ&v&zvk4r`6mW5?~NpuRAa&>ul@m&`oe$~Er5q>{48Lvf?B z9^D~~oe_CRIGy!C)_g&6ePemu6RW(|Rt=iJbLXgDuc-G;TJ*XYCY)ESUDZyxw&v%# zQEi32U4Phpwd&{&-HG(r%5R^(GWwQqb&toDAM_EQzcJ^YiN+ld{Tv^>bN{hmt|XN*fsj(s3)L+_L^&-MCc$@uz{Rbl=|^A+zrJots@&#dcnD01ZI+HULi_K5Vg zw4Qm>ee3>d^@XF$YTGTol;t12`S0%!)^Dmj`}(x06CcmrIBD+id+I-q>b!Y#x3yWB zZu?vZPFk#vI=Z*6RaO0CpReueebvilVw&q3-=K<(9>U`EyM5MGPd@B+PBm;rRiBy_ z?(cW{`g!+d_w<_))xD_hk=Evn?cdE0>iZur^`KWxx^|yVQbygSzPfeJ&Z5^drmpxh z!L8mjcKPI5^Wp~%w^`iH>zVl}bK1VXXV?6h@dFe$Jag*$Uk!ix(0j9{^{V;e=9uo6 z*1vn?Tx#l$UNxpQ(>(m+bJm5YhUTmro!)wTxQ|}&-Q8>H*x+j?S9R-jzI5iM9o{vq zCTB)YQ-vo7_c@g zh-&MjJLx*}aanGdL^`Mz=g~g+YLc=q)Cj-z>!e62`^n|;cjk=z_{Ar^ zJl_#__q}jSsI5Lw*WY(~YC!i(H?uv*>1T|bRWwys*wwqHdU@*5kjG}!y!y!I{ofwj z^mUd0{uy7?ZtQ<*$mUJ2%^PzgtNy6#do@d@wF>vGX_eHWt9UN`;Y_#WcJawa_jc_# zX4%Ce*C^fj+0!2H>vM3nCaQb-jkRk$V|xCfO7IktJcsvsyjOaWYx0qYligQ_2{(?M zdAeruQJ3$gy%Zh4v1?vXYRjjfj1C|9MonuqRRs484bauhkqvQHbN;&ZPrkG7@tA!o^SvlgFR{PCr}b3C>$Qum*|W7@E~*8B8V&#cdGv$5-alj>rk z7B26Q>p9%L-+OaBrqwJ-fjtQ?j_#FSe^Gh9%@4_~+EmZ%w<=w@($(Ad^o*#^akDcr z-PE7Q%sn(Zp~&^iuoLfpzWI?c+kZY@SLI(>Rle?%K3lfwzB*YoBxFtfGYfys$UIZk zujawRn#aOUj|~?l92$JS;CytwaB}*JON)N_rl?r`+W@1!@B6oemu_rbSN_t-&tJMF zY!B}_sNeZ7PN&u05=PX`EBs95^=?G3h+atwmv0u`_fT3$@S1fmrf+_Jj^D%Iygj&T zw@&eiZkl^q7t^%=6hi~nUwYdua9#F1gKu@@#JH2I9!okFvLicZbM_`+$Hl$oPFnB2 zs*77@|I=!renDle`%B8yHq{H$KE_u1^g^op+BVfCVJ>3WiD`aE=hn6hP3i>=7UnWo zmwp*T1}u&0VyFxKQ?32&%BIsx^5P1oc8yDnoG%n!_QXP4u3m4Z^iB~5fxZ%<+t9}7UgfuU@R+RM)qia1 zec%nmE2=A>!>ld6FP$OiI_NWN=C^W`ow*ND|Rn?J)Yo@xdZnL?z zO~Go8Z2LB=T&@2jOt^gPKfgbjeYfZ84ojXp zzU0Q1Gq1dGS6I-y%GFQ)b?W;IlNYvHeWs@4W7judd3V@vi)zL zTRl#EJ}oh)vofkz+pE4iGM-3!`u#Qc_D0gq-Vk)@2c?5 zsUzz@`|vl9kH%H68TI6pZP2Dm=6{m=A0GoOoU|Me`z*Sag| ztt^?Dhh1Pklf3bzBw-S$)G&qdx%b6Eg4gK_DH~F^3x7fw(&(Z1==dY6zqcG!xSSjh zqHy7A%d{t|h1E%?eqR*$?2zk+;>SgKOnfpk?Nmm@@$f-$-vOrsE68SH;}X78xGV!A z`(E966xCM&G#$5FU!lzajSt+{$+7Pfnj-AnoZ5nUr6==lKp+m1_(QO*J7GZM#h~Q_ ztt0aNAMi>n9k5--1m0stvo=3QL$`)t6dFZxX}H>$tjjfNOqIm)5(j&%z>^uGS!fJ6 z$rc(-I@olA7g}o?#Gt2}5CSoZ)U4#R7`1AGTH(!cw)(Dl`-$EuI&+?M_wgZ!u>CyI zdhm2i!3{WQ(uq!1rN&MHC+-=gT2d;9$4Q+{Hl3j5hsRKdGqmsU?K#ASk?ziE4Z$Hn zVZ(_%iN{knM7soNECOPZ|NLJ8dl<;>Xh?E+p~VWX1bRaQlFbi;(fWl~vw&hIIZy(D z{lh|{*)!uUsSx1=*tTMvMgm!QO*uMfG{Y;H-WvX|i#I;t<+40xO{~6~s@=EGD_D|5 zcIWjYrbLdj91JgMAPaO#O|${x#ev)rY9i2ohng1wY%D?8+PJr+=8EH6E5#pAC~Lml z8ouYI-)H|GHPNby*BzxM0{wTWiC~*e&6y+KNclarPwcaYrnO24eI&HW)O6Z^YQdq* zY5zfsbhB|>>fa;e$I0uGUpX)%X72dSfBefhP6dm3vvJHn*Xu6#)RSrJ{&u&NKb<$c z$vC1w#!E|Y0(U78aKrEbuor`0zNku0PE5~AONEo}d3xAvsQ_@rrFwf3NVSs?fm-_b1b`ualo%K&tDep*n!y!b zn=fk2V!2kY7cEB7TxrM?%XO9_5!3^+^)kTIgKiFLv^0*Up9sVxg^}@D-spo_qfdOgaL@$N0@;TX;X zGg%^>@i@FvS!e?(Vqy{L?9Mw#TlP+;5%6MPhiI0dfu>#p%D7Waqa#$S*`xRv_1d%j zC(l>C{b}gtzrsClDp=7DHFaE=$BU2LoMDA&5GV<8vuG*OiaOkFE$VSAv`99;ig1(! z%Jx#&3CKe7aEHFmfaewXiRDFFla^#Pll`VJBEb$)1eHs+mx}lL!}uM1UjUhfA{v~I z_>7keS>ea0hB0Xv3>RXBV=7neYJmO^mUm61FxMS;UZ_kRX5f z7bem}VdAs=#6&}0fCwjM;MQh(qKVODM$IbFH}Xzio*wOFC()?2#U*-R`G6w5SwB+b zuOjUE7V}_(sRcKN=aJJuMCb|maFz|x#VnPuqqq#uvJv5g4-IHycM``)-@ry|?&%?l z(W>~wR52zyOP!V?W+f?b{_GpoADFw9-s>AjMgT2Vp;{i{PSwxw2kdDNP zOg~4JFkh>13`ewr^g%=cs)!d;RfC}fQJlq#a@x9>3QS7Ptf5nI*gOc5k}Xp?Q5-5J zrm9t$S*jRyVp^)0txil%R8MS#h6sYvk^=LHEl{lqFVHqgSN&(VI*21 zj~0E!pn!0owKW|^rzcKH%TZ-cQIAg(g9E}Nf+9u?3k?|-F(PdEh+)I*(~6-1!C}GS zBSMCShlURy5itU0dK@Q&22cQ^I4mG!cxdqO@X(-$;D{07!vT(`2V)nQF37ngRHO$P zkoG3iLfH%s0&Q3g_NhbHqGxo-fru7)cDl=0$_p|jQ;VnV;I?lzWNFCA1_9VpQJlaT zPGwqngx;zzE!G05t?0^2rTi~~9I|2}sz4M~5)5pED|CpLO%0&-@-%Fhx=mf+r~_%6 zx`I)pOx{QMY)(_5D288IK!u-J7EtMf|7YSvm~QZh z=`Dc2wbw{>Q$aBpLpzRaIH0Y?o1;Nv0Ea6;OxmEV^h3#M6BI44g04VRD53pFM~cw_ zv~t!_2PsfdjE2Z&Gx#jYkMniCC~8bhHL5#b$^(1N`{ z7q@xURLIFfd$^8*fVNQN={3bAiZVZugV63g%uI8gy7cvwJ#f)ym4#^A%o$8w`+;O9u>i=~id zI8BcOBtQhENJKzZ$iM^=F+ylsd6ijPLly-o7b@W^hJ=R)1o^=5uLubr2JhC`N-5Sx z)l)0S7P{!DDUk3?y`i zoS9m)QICV31t==d4r3#*k6APp5M{&!bHLn6F%OPTDJ6<7%CUDgAa;|XvYzx z7JxWr1PO?;2}BcvL33I`j=?CBincDzj6#hfKzKT!|CD2E5&o#W}rg=N_0vFk{kjtH7$VkS4U`(;{1RS zY5JzjAdsgJF3uwUp~Bz9X$4Hf(p4>*Nkpej5QAkJ_r&DHRG86;A=WUvMHnVpVJ23h zwN*ClSeZqNcsCOcK%(eCjFP69Fc@m`A&^W{=xT&kz>SrVF4)kK17wueTD-VgsX)F;Tnk0|Q$qs|7a*c2Gea;B1;P;-VxzMFmDO4^ol62u zOfn*xV60S7cE&X@AM3bEEv$e|fJlcaZgRB_+r)Tk&CEtt$K#eHGoy_} zw5VbsNkp_;;G_~x>LM`nbiAGQlRC72Jj=0F@O%wjGaeVi{E z4Wd*JVvE5l$d*yD-V6ekE*dnga(828Rg9k4Q~_+jbg60 zNC(yqgh0Bls4!>?!F*W;t?0wx04qt{g=e=mX>LdAOmS2rQJ~IbBmpNknKgq^4J;Kw zHpC*;u*)YmT98KKxhMru9w4|%wK#@mx0eL+N^p&b;?zXFM@no@5u#!_(SBt8g3eOF zVkG06eD0ip`j=T(Xi&&FozsNP!OVEPwu&iGB#hCN+Fuwkxy6A|1C$>^6@K?2t`Ud< zvv?4Ie1?DmkPI7?XB9eYnLZ%qtk|+nXafVM>a;K#ODoEej{^Z>Q2~2GDhJ48=dhv< zZ)&Qx@moW@k<;14hB=Ug0=`l)jpB=W3A$JiU`tVDilnV^!A8`#T+);7MS2ufOze5c zYMCIT8k=+2%J2qxWSH`C@m}!qG~^W#-9Z%1;Q0aK)^?<{?-+S_&N~d9be@K+VeR5l zr@?V0lQSeH$;E>-85@WqxcuPx>^2~X#l(51kz%AWK)NWX@eKunAJshrpm#b+!OWmT8wjea}f(Z|VH(8LxLC`Aln3QY4$tbdH0|6;80W^zS@Mub$ zQG8Wqf>iP3=${-8lx)!+bHMH|JI6p#&SAxW2=L-b-~9yaJ? z=g5@+#^BjGq}^<_Kf@MI$|knGh#QlV{8Yn~rD!?lnW9u=oK6go08$TT?7G zGhM-^>I_@^$}a_dTeo>U}aN)6*rK`5ERUuQQ65+gUSl7%_CO6B{0#s8PL?bze9&~9KIG{6W4SDdCk5qk_+`5Y!Me&ZaI&O3RLNOW(qRcVH7!;pjzHyxM^aKuNl{LilB~*sbYf@-&fF3- zrhHZCiIJw!1VMQ{M#r0HZBrsuFbEm*sc}jN5lq|I>eM(l0GFh1^g1`#G z^+jH{tc1ik^%PZlR$?-&%VTU%FholWx2ZsJwyvrJGen$TQ%INT`LK3fU{|)Z+-)tK z6BotQ-I608UjG%-iA zwoD7(*Ps;6(?Vu0DxgqYi(nXoDghLSVGxE`ox#7vWGn=w4CrWr^?8#Sh)m-s-2z|; z2Iz#$8rof9EX6t(f<|JXs+Sq5>Pu$M)3w?X0xNGiZ%o=Fu{nrZjCrkP8MNAb0z4PY z8Bhc8Js_0#B@k27)GAUK=Nxe1;1(2bnbzk`%Jzndx3OzE}VZf2Aiz{xsyh)wFM#2-Lv6`gXRI%KldIZ1 zF|@NR1*w)O(J3Ha3tybz9}QB-i3Qt0DCsDXxKe`c&PW81Q$eOU2QnSs4N4yb&5q5P$$uR)2=Ag31KkT0-*? zoe{}p<;^CxMUggdI`QO!W`N<>LPv6GnvX@ZX0TEhbT=8(6Iz>LVrn*nw-@aXyaf@O zMqjR}L@~^PGI%B<%2A{oYfOALYncg_b`Lam5ibnPtA_a7v>25glsfWuV~va@04qjf zBO)~506rtZNv=(9)}06jI`9J_*t1<@!A znLkaj>h+`uoHvT%Sb|FcN^sI6NZ?58DjK04XwL>CEQ@GBQ?+XYQV?kft1ieX&x~)h zBS1bBX9^p}1JMD^AcJnp{=k_26hEgWkXbe@h>r%8e+iwT1@MmaXCn#Ywh%tD5Q@o3 zPAVKm^d=+ zGzxqtCV3`S1sOk6c>#6_s%IsFkCasKO#1-k)(Y0f*=l@qx@Jk~E}~*;R?oH7Vwi zvSi?#>nnzVLxeRac=myZoNZ7ALy{1Ok(vVo)Z{$RNGw&BbH6+n&~hlO!$TQl3QQo` zlEFc5l>?9jE%FE_5PUWXCio61B&P-=w1X0ER}-mJ`(cr4nP^a!^bEERAR80uNQvGR zU`3$wDqw1=ZjwngGce)giLLv>TvBFNHvl7mnaNf)%}2q+NW;1xy1cWYm#9K?Lzk>) z5Q4F(ofZxg0TTmu2{t;)6%d^P?d()eOLIZo#Z700amo9M^H7ziJ4j`bNa<*bf(-k6a0dJTA6;ohqSwW27Sbp$A zg_0?hp$?K!Yzg5A%R+8e0T>2xAP{wsKWG%(M)N_|YMC1)4jrl^#F9Df0m0&QY`va0 z7j|(FbTEBj%s|yGs!nmgCaIWLCXtTBy8x|a5#ccI1cZ?P5#c^y89>JkfW;;^ zq!>~N&g4Y}H3_4Ijg0x66Y`+3KnI6<5f>^tw}(y(#Ny;9v-VKhjY-=ODr!pt1j~j!_^`p?Ll%gt4F@E91WusJ$rV5GNA>+9JV@>Wu(yvf-pii;3lXO zQIL57BsD+HG5xyl?2i8cN#`a3@V`Uk6HF+80P_o(zH< zr?n#r(2Uj{l?~RzB>b4*B?(9ziy=pf~Ioypz*Bp!324IGHlhZ38%5Hk`Av#8gnD zTTcM2Ji#1`0+Tm+ChXEF)=FqQY4h6*L8$v6TQu74NPnCbOOtkobYp{yj{`vsIx=|2 z!>$F8pk``eL;(el5Ck*;e<6(t7#mHwpuk#}8|eI4l3aI?lGs;ikFsxW;NCRA&Qz{5 z17hG9R*GT-49M{DTOCZ(aajwb3^?i%g^8LxWXcED%xoE;mM#~7NGMP*8vR&xGD zH%au&vVtPR1?N~vH3v5%jv~`abb5sqa~NbGsnKK`)UY&rxZp0x!9t_+GKShZM$+Ei zz**0x6`-0l8N-;f0d_DIbMf@or;MAmX>WT2tX~|u*feFXP54S<- zyx!b-Q|@`%02FmUZ2&{^=9KX8v!Wz>lvExY0#3KXu(UR~Oh6~I2AtLDWKs-8 z7RpyP6NJSabeAh($`r&(BrKQzCCa!gKe~S%r zez>v1SOU_78YdAQ-ptaO4h$hUa}_2rTq%LUgyBF0x|~or2B!iUNS~KSpgcz@%m!t%dIm%1a62ed(h88r1`reLuBcQ$UvNI0uA zENR66a{htj1VPG>qy|-0z&xiw!b#L(sMug@BjhEKgm{6&Hdqf}LlN5fQN5$xo%9ww zjf!+$om?USmU%@+^c+UM0S6?Lu>?HUU_TN?h&8gjd`cWX+>(wUUlp}?Jm*{wrk{ol z!uRE}YEo)&ac)y6Q5>NC8k7e^GIi9KCpS~T3@pJFq0){*I1dDEg*HD6-GvGU>8MjEDO%7;P@XZJAjJ$79ZHG zL+D^vWs=*$q&f@jTVRDmdlRg<6E`z?Sp?GCUKDfQP{faVgZR@r5+}ifIP`g{6w`PIKIh>E{8D6Z&A`Hy1u%_e@`C_E)q&U*)J8I|Ag3K5{YjvF`iH$2QxoFj(k2-B@%RF37&*ho zE{vSy6J+0&S*PHK@30<-#ZJls$5CRakQD_hrP3QcSc$s+S-k`USw=2t0MbP0!f=ue zI$aN9u{hRRA<0%K#R-s@r)`*mp^F4lFrQ1DcG%5!L894E-}+9dE_G$P6zJp_G|(8V zC!smi?c{9&Ow1Hz!fFz$lLM((+k-+Ce>P#$qT zi^v8v1_BMqN`Xe4h$Vm|nS}QO6WWJ?io{bwF)`UG*~v;gK#?R0 z3K%gQJ46cWMPqOUh>VeN7#Hv$`pdDci`W9eSu&^zpH=U)Dwt(Zb0kQ)s zY%~t!R!g=Z1Vjks%w`OKnJ6ic+G0+fXmhrH5UZGf0)p<)x3k;n{`DN~Y*E-ArGCVDob z3OkXJvH~&gpnU+&qyiW0Qd1eAuh0e*27na?Z2BO#HIk+Uasn!2FdFF1Mlq1>DAquJQU@a=fsadGY;O$FGLa|vX2_9fZZYa11e8q>_)OCz)rYkR zU5OdaW&tjv8#}DV7ode2IEC%8wrZku4vJ*Fn){TxQymf)9>{w`+#Ly-X zi;Q^qgf^ex%n_k8dbG2^$aXKdijYI0F^Ln>VGS9t6Xq`leVDG>P`?*!OvzWp0s6pg z#TqcQX~A#-m9%yT0u9*)p#dkN_ksy_F6nU-6XzdNFOEAI^$oynn8Vv#3?_#nZN4}5 znF4e@H^@rBqeCf`w2Tl^TLjoK4($tgz78K45lJ|B4KzA-6c6+QD=)()Ca4Hs=|bC% z&Qc01e1Tx&w(v(dm<P3wS<)=;KHIRc45n1R^9;R7Wp#6iUk2CO8jHkr^mm`uY!m?$&FnPIGxFP28i zM{U_S4P!4$Mr0*DUV^N>^Kih6*}nq7wqrds?*UZgWOGQOo&v{605Kz(xYI;XIl$e@ zNj_-}Io;}?!B&Uq*a@s6Qvz~uL~fvh1YlX@z%iZ_^$9yA!Fr?-%q8jR zVuaB`+Yd{4eP0HWx6H7B}NGLr+BG^g@9Nh505_%fA-8m&T#W$!fk==F^^ni+) z(u~F?x6NzkryomR>h z!Wv>rn4KNI0(i^&A&s)YA&Ri#L&jA5150eSZ}D1?#U)-i%rwfS`2?Yh^hPK^r2vRD z>ITJqd^CE9#qLE7EX=tP!sPhbIZ<-@%WEX_^YP;zk^n{CkZ^Qpg~cYomI_W6FDYt% zHZ`<8YZ?rr5>en(7QXUGDo)xa49y9pu+{`~r+jn}B*%QyDC;M|*ug!H=jgJJ4**I|m{KR^u;RyzPR zs!d0>wE|KIybmJ3r^Tj?M5lLL?n3(s+WWvBR03;WptZp{c4KJ?j6igU2ow^Ng@PLV zx2#2Tunma_`z>fjLIGy9A}cVUT9D}ns0L6&qNwUiiZpOM4IG_^2L=|Dq5^H!!2T+1 z73fBo9jVcv7}%vXMs#c#kO(C29m;eo`Gj=@Jf>=_mm3@|o;-9ai8 z=uCP63KX3I9Km!rH^-^20aYlvHjNX{A|QrX1ga?#LYrSmRDQM}4uGIyT}Y=AWvDEG z=8rt2Dbi}-&`dpQ=m-{GmHolNBmG z-nR6-mX^-H2E|K|ij+O@IHz^bZE5M8r+EpoLZwF@=d|>omX_XuW#lDDMamv{oYT^? zTUt8rUc3ZZq0%Fdb6UE(rKR%^DDx7eB4rOe&S~l6T3R~)E+8*KR;cvI-dV0ZqpQeiQCA3nxeWQ#s4IJnEp`zuCz#LaD+b8p_Ytr`V`?(nmM z96wCsltNJqLI}Ha3C|nf>9@!WKlo(e-s?Z5Y+5j5`oXzpLWVH__FrSm$Bac(zZ2*elO)+d99u^x*WI8#m&YeNr)F zerW&H6gvRB+514-a;$GozuY*UrTL%#a$~b;*7u;e@`vA_jjjGTEo0r3zpL&X%>w?< z(#+5BN%!9KN>X3{?6(BBEsEiHj%KV&2>;Dn%@rwm9DnADSX&;z z?k+;J_4Ua^SiYcFaA1x=N{#A z-*1^5dw6bnqY_C@4zpcJx1}jJe93_@7}CvY#j7BRQ&Idf$h>A(yz{wbZCt`mkK4KR z?AbVd%Je2zyxAJN7pq>2D*kd~74HZ<*zAheE)QG3@|bJlTdwC`=w!B3{(BYgYgchJ z(#d}q?X<;3Na?i2tJ=1p;$0@UtKGk{BK7eL18=6O)@1y974P$}ReW(vT73J(VI#YA zxHfKA$>+OHM^`BSy^8nyCl#-1ti?;fUejz=|1JDX;ii|*q%7$AS?%HO$Nm4kiueB~ z6<-YU%PFg;gA&^8iho#Ar#w1%TXOaO@y6A;-?jetDlYz$ieGH3#ixQXs@WB<(zW&* zo_8hsh35yhZFOz^rGKyD-R&yQUTtnpg{V+M?gUxC4`gFQ+ zUs7V%f3M=+b`^&vF7?Kl`9IZ1s8g3b+IgD#6U|*to4>X(o9PMn7EqyP>WqLV|6_{Ub5XcI zC~>E$Q7UllR?8|XSvS#2h2IAxE|($jV0GdRg*V9+Rsa;qCDDw%yVxwk@s&vsZ{&KO z-8n%8V1@?EmwJ|nvl)A}-gub_)}RJYTsLpsAjTR10}4SP*&5W~2@%AL*MP4UNj1O- zo`?sEm;)}S=J{bqcj*#Se(vV8a(&Ir4y^-sceuK1Z)&OUr>1LbGWHC)5KZ$74Hhk? zLI;U)7TFOxVzb4@YninJe>%Q3dhNbF*S6m%{iRX9aYS)m>46L_r_DBiCE!3DiH%k) zO_PE0_T@lXew&+9YMy{q0jJb#(VXI8?;(>a>!ag0R@W{Y6+dBCld0)c7A3K&G+P!a zD~4~~yYRlGs=14*RxKIO-zgOul|>y}G89cXw|3vkJ?=^CO}*A!jh&m&WGXa!X2sXD z8f$(oPk7q4jG}yUB1~riCG2jWkSp_ojwNtP%BjH9P8sT=YYV#Po~>y+OFX}iU#$3T z+}U<>mj8RC#GWOP2JLD3Uy()a#_PI zCBu1T?C&053>YmBbJ}O`XQ`UqXI<)6eEGq1Au-Ezf4|o&<=Hf+jMiwMMHQ>r<3O9# zQ$4Qd&Q96-;0Hg?IN|!mzeh$Sj+~TE$vCB@WZb>?R^Cf1qLLo!-u27*UtNi985vJN zk7za-LqE)aT(Q17>YX{qI>%2N_AirBPD-a_oZ3<{4ho5`X!qOI=%*`2U6|AJi~Cwe z#=|f~H=B&vu`BDYi5pR5OO7%|gqX)kH zbJpK2qe4(i%tMgkBp|9vU?jg#NKw>U7! zy=7DoL0fIMc%A#v-eIx9`sm%8UiiNLZ==gUr$S*%sj#`%BQI+E8dKh!(PjRW#B0Y} zMg^fIh9lLYBX7o-)ynzrzay;sE9BWGQ=w%z*1EQ!rFMIX!=?;3#l6yg>e_?4P7|7p zqsSVBYqxK(cO|Bni2YE_h)*@tQ$zgr@WnvhZRZM#^g~f5zNc z$qWjHP*52j5*s`$CL$zM84?~AstgW~4ULIWMu&!lD2EMGs)W4{`>t7eZ9`SSs1H6v Y(n<3<2Bhrx@QhX;?ePg>@0kAo0iyMjng9R* diff --git a/Shaders/Private/CesiumBox.usf b/Shaders/Private/CesiumBox.usf new file mode 100644 index 000000000..51eeec486 --- /dev/null +++ b/Shaders/Private/CesiumBox.usf @@ -0,0 +1,68 @@ +#pragma once + +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumBoxShape.usf: Utility for intersecting a ray with a box shape. +=============================================================================*/ + +#include "CesiumRayIntersection.usf" + +struct Box +{ + float3 MinBounds; + float3 MaxBounds; + + /** + * Tests whether the input ray (Unit Space) intersects the box. Outputs the intersections in Unit Space. + */ + RayIntersections Intersect(in Ray R) + { + // Consider the box as the intersection of the space between 3 pairs of parallel planes. + + // Compute the distance along the ray to each plane. + float3 t0 = (MinBounds - R.Origin) / R.Direction; + float3 t1 = (MaxBounds - R.Origin) / R.Direction; + + // Identify candidate entries/exits based on distance from ray position. + float3 entries = min(t0, t1); + float3 exits = max(t0, t1); + + // The actual intersection points are the furthest entry and the closest exit. + // Do not allow intersections to go behind the shape (negative t). + RayIntersections result = (RayIntersections) 0; + float entryT = max(MaxComponent(entries), 0); + float exitT = max(MinComponent(exits), 0); + + if (entryT > exitT) + { + Intersection miss = NewMissedIntersection(); + return NewRayIntersections(miss, miss); + } + + // Compute normals + float3 directions = sign(R.Direction); + bool3 isLastEntry = bool3(Equal(entries, float3(entryT, entryT, entryT))); + result.Entry.Normal = -1.0 * float3(isLastEntry) * directions; + result.Entry.t = entryT; + + bool3 isFirstExit = bool3(Equal(exits, float3(exitT, exitT, exitT))); + result.Exit.Normal = float3(isFirstExit) * directions; + result.Exit.t = exitT; + + return result; + } + + /** + * Converts the input position (vanilla UV Space) to its Shape UV Space relative to the + * box geometry. Also outputs the Jacobian transpose for future use. + */ + float3 ConvertUVToShapeUVSpace(in float3 PositionUV, out float3x3 JacobianT) + { + // For Box, Cartesian UV space = UV shape space, so we can use PositionUV as-is. + // The Jacobian is the identity matrix, except that a step of 1 only spans half the shape + // space [-1, 1], so the identity is scaled. + JacobianT = float3x3(0.5f, 0, 0, 0, 0.5f, 0, 0, 0, 0.5f); + return PositionUV; + } +}; diff --git a/Shaders/Private/CesiumRayIntersection.usf b/Shaders/Private/CesiumRayIntersection.usf new file mode 100644 index 000000000..6b0c30521 --- /dev/null +++ b/Shaders/Private/CesiumRayIntersection.usf @@ -0,0 +1,103 @@ +#pragma once + +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*=========================== + CesiumRayIntersection.usf: Ray-intersection definitions and utility. +=============================*/ + +#include "CesiumShaderConstants.usf" +#include "CesiumVectorUtility.usf" + +#define NO_HIT -CZM_INFINITY +#define INF_HIT (CZM_INFINITY * 0.5) + +struct Ray +{ + float3 Origin; + float3 Direction; +}; + +struct Intersection +{ + float t; + float3 Normal; +}; + +// Represents where a ray enters and leaves a volume. +struct RayIntersections +{ + Intersection Entry; + Intersection Exit; +}; + +Intersection NewMissedIntersection() +{ + Intersection result = (Intersection) 0; + result.t = NO_HIT; + return result; +} + +Intersection NewIntersection(float t, float3 Normal) +{ + Intersection result = (Intersection) 0; + result.t = t; + result.Normal = Normal; + return result; +} + +RayIntersections NewRayIntersections(Intersection Entry, Intersection Exit) +{ + RayIntersections result = (RayIntersections) 0; + result.Entry = Entry; + result.Exit = Exit; + return result; +} + +Intersection Min(in Intersection A, in Intersection B) +{ + if (A.t <= B.t) + { + return A; + } + return B; +} + +Intersection Max(in Intersection A, in Intersection B) +{ + if (A.t >= B.t) + { + return A; + } + return B; +} + +Intersection Multiply(in Intersection intersection, in float scalar) +{ + return NewIntersection(intersection.t * scalar, intersection.Normal * scalar); +} + +/* + * Resolves two intersection ranges of the ray by computing their overlap. + * For example: + * A |---------| + * B |-------------| + * Ray: O =========================> + * Output: |---| + */ +RayIntersections ResolveIntersections(in RayIntersections A, in RayIntersections B) +{ + bool missed = (A.Entry.t == NO_HIT) || (B.Entry.t == NO_HIT) || + (A.Exit.t < B.Entry.t) || (A.Entry.t > B.Exit.t); + + if (missed) + { + Intersection miss = NewMissedIntersection(); + return NewRayIntersections(miss, miss); + } + + Intersection entry = Max(A.Entry, B.Entry); + Intersection exit = Min(A.Exit, B.Exit); + + return NewRayIntersections(entry, exit); +} diff --git a/Shaders/Private/CesiumShaderConstants.usf b/Shaders/Private/CesiumShaderConstants.usf new file mode 100644 index 000000000..281d83137 --- /dev/null +++ b/Shaders/Private/CesiumShaderConstants.usf @@ -0,0 +1,10 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +/*=========================== + CesiumShaderConstants.usf: Definitions of common constants for Cesium shaders. +=============================*/ + +#define CZM_INFINITY 5906376272000.0 // Distance from the Sun to Pluto in meters. +#define CZM_PI_OVER_TWO 1.5707963267948966 +#define CZM_PI 3.141592653589793 +#define CZM_TWO_PI 6.283185307179586 diff --git a/Shaders/Private/CesiumShape.usf b/Shaders/Private/CesiumShape.usf new file mode 100644 index 000000000..8c717de56 --- /dev/null +++ b/Shaders/Private/CesiumShape.usf @@ -0,0 +1,98 @@ +#pragma once + +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumShape.usf: Utility for intersecting a ray with implicit shapes. +=============================================================================*/ + +#include "CesiumShapeConstants.usf" +#include "CesiumBox.usf" + +/** +* Converts a position from Unit Shape Space coordinates to UV Space. +* [-1, -1] => [0, 1] +*/ +float3 UnitToUV(float3 UnitPosition) +{ + return 0.5 * UnitPosition + 0.5; +} + +/** +* Converts a position from UV Space coordinates to Unit Shape Space. +* [0, 1] => [-1, -1] +*/ +float3 UVToUnit(float3 UVPosition) +{ + return 2.0 * UVPosition - 1.0; +} + +struct Shape +{ + int ShapeConstant; + Box BoxShape; + + /** + * Interpret the input parameters according to the voxel grid shape. + */ + void Initialize(in int InShapeConstant) + { + ShapeConstant = InShapeConstant; + + if (ShapeConstant == BOX) + { + // Initialize with default unit box bounds. + BoxShape.MinBounds = -1; + BoxShape.MaxBounds = 1; + } + } + + /** + * Tests whether the input ray (Unit Space) intersects the shape. + */ + RayIntersections Intersect(in Ray R) + { + RayIntersections result; + + [branch] + switch (ShapeConstant) + { + case BOX: + result = BoxShape.Intersect(R); + break; + default: + return NewRayIntersections(NewMissedIntersection(), NewMissedIntersection()); + } + + // Set start to 0.0 when ray is inside the shape. + result.Entry.t = max(result.Entry.t, 0.0); + + return result; + } + + /** + * Scales the input UV coordinates from [0, 1] to their values in UV Shape Space. + */ + float3 ScaleUVToShapeUVSpace(in float3 UV) + { + // This is trivial for boxes, but will become relevant for more complex shapes. + return UV; + } + + /** + * Converts the input position (vanilla UV Space) to its Shape UV Space relative to the + * voxel grid geometry. Also outputs the Jacobian transpose for future use. + */ + float3 ConvertUVToShapeUVSpace(in float3 UVPosition, out float3x3 JacobianT) + { + switch (ShapeConstant) + { + case BOX: + return BoxShape.ConvertUVToShapeUVSpace(UVPosition, JacobianT); + default: + // Default return + JacobianT = float3x3(1, 0, 0, 0, 1, 0, 0, 0, 1); + return UVPosition; + } + } +}; diff --git a/Shaders/Private/CesiumShapeConstants.usf b/Shaders/Private/CesiumShapeConstants.usf new file mode 100644 index 000000000..5206a9c6d --- /dev/null +++ b/Shaders/Private/CesiumShapeConstants.usf @@ -0,0 +1,5 @@ +#pragma once + +#define BOX 1 +#define CYLINDER 2 +#define ELLIPSOID 3 diff --git a/Shaders/Private/CesiumVectorUtility.usf b/Shaders/Private/CesiumVectorUtility.usf new file mode 100644 index 000000000..a9f2b59c4 --- /dev/null +++ b/Shaders/Private/CesiumVectorUtility.usf @@ -0,0 +1,36 @@ +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +#pragma once + +/*=========================== + CesiumVectorUtility.usf: General utility for handling vectors (e.g., float3s). +=============================*/ + +float MaxComponent(float3 v) +{ + return max(max(v.x, v.y), v.z); +} + +float MinComponent(float3 v) +{ + return min(min(v.x, v.y), v.z); +} + +bool3 Equal(float3 v1, float3 v2) +{ + return bool3(v1.x == v2.x, v1.y == v2.y, v1.z == v2.z); +} + +bool IsInRange(in float3 v, in float3 min, in float3 max) +{ + bool3 inRange = (clamp(v, min, max) == v); + return inRange.x && inRange.y && inRange.z; +} + +/** +* Construct an integer value (little endian) from two normalized uint8 values. +*/ +int ConstructInt(in float2 value) +{ + return int(value.x * 255.0) + (256 * int(value.y * 255.0)); +} diff --git a/Shaders/Private/CesiumVoxelOctree.usf b/Shaders/Private/CesiumVoxelOctree.usf new file mode 100644 index 000000000..2fd410ef7 --- /dev/null +++ b/Shaders/Private/CesiumVoxelOctree.usf @@ -0,0 +1,307 @@ +#pragma once + +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumVoxelOctree.usf: Utility for initializing and traversing a voxel octree. +=============================================================================*/ + +#include "CesiumShape.usf" + +// An octree node is encoded by 9 texels. +// The first texel contains the index of the node's parent. +// The other texels contain the indices of the node's children. +#define TEXELS_PER_NODE 9 + +#define OCTREE_MAX_LEVELS 32 // Hardcoded because HLSL doesn't like variable length loops + +#define OCTREE_FLAG_EMPTY 0 +#define OCTREE_FLAG_LEAF 1 +#define OCTREE_FLAG_INTERNAL 2 + +#define MINIMUM_STEP_SCALAR (0.02) +#define SHIFT_EPSILON (0.001) + +struct Node +{ + int Flag; + // "Data" is interpreted differently depending on the flag of the node. + // For internal nodes, this refers to the index of the node in the octree texture, + // where its children data are stored. + // For leaf nodes, this represents the index in the megatexture where the node's + // actual metadata is stored. + int Data; + // Only applicable to leaf nodes. + int LevelDifference; +}; + +/* + * Representation of an in-progress traversal of an octree. Points to a node at the given coords, + * and keeps track of the parent in case the traversal needs to be reversed. + * + * NOTE the difference between "coordinates" and "index". + * Octree coordinates are given as int4, representing the X, Y, Z, of the node in its tree level (W). + * These are not to be confused with the index of a node within the actual texture. + */ +struct OctreeTraversal +{ + int4 Coords; // W = Octree Level (starting from 0); XYZ = Index of node within that level + int Index; +}; + +struct TileSample +{ + int Index; // Index of the sample in the voxel data texture. + int4 Coords; + float3 LocalUV; +}; + +struct VoxelOctree +{ + Texture2D NodeData; + uint TextureWidth; + uint TextureHeight; + uint TilesPerRow; + uint3 GridDimensions; + Shape GridShape; + + /** + * Sets the octree texture and computes related variables. + */ + void SetNodeData(in Texture2D DataTexture) + { + NodeData = DataTexture; + NodeData.GetDimensions(TextureWidth, TextureHeight); + TilesPerRow = TextureWidth / TEXELS_PER_NODE; + } + + /** + * Given an octree-relative UV position, converts it to the corresponding position + * within the UV-space of the *tile* at the specified coordinates. + */ + float3 GetTileUV(in float3 PositionUV, in int4 Coords) + { + // Get the size of the tile at the given level. + float tileSize = float(1u << Coords.w); + return PositionUV * tileSize - float3(Coords.xyz); + } + + /** + * Given an octree-relative UV position, checks whether it is inside the tile + * specified by the octree coords. Assumes the position is always inside the + * root tile of the tileset. + */ + bool IsInsideTile(in float3 PositionUV, in int4 OctreeCoords) + { + float3 tileUV = GetTileUV(PositionUV, OctreeCoords); + bool isInside = IsInRange(tileUV, 0, 1); + return isInside || OctreeCoords.w == 0; + } + + TileSample GetSampleFromNode(in Node Node, in float4 OctreeCoords) + { + TileSample result = (TileSample) 0; + result.Index = (Node.Flag != OCTREE_FLAG_EMPTY) + ? Node.Data + : -1; + float denominator = float(1u << Node.LevelDifference); + result.Coords = int4(OctreeCoords.xyz / denominator, OctreeCoords.w - Node.LevelDifference); + return result; + } + + Node GetNodeFromTexture(in int2 TextureIndex) + { + float4 nodeData = NodeData.Load(int3(TextureIndex.x, TextureIndex.y, 0)); + Node node = (Node) 0; + node.Flag = int(nodeData.x * 255.0); + node.LevelDifference = int(nodeData.y * 255.0); + node.Data = ConstructInt(nodeData.zw); + return node; + } + + /** + * Given the index of a node, converts it to texture indices, which are needed to actually + * retrieve the node data from the octree texture. + */ + int2 GetTextureIndexOfNode(in int NodeIndex) + { + int2 result; + result.x = (NodeIndex % TilesPerRow) * TEXELS_PER_NODE; + result.y = NodeIndex / TilesPerRow; + return result; + } + + Node GetChildNode(in int NodeIndex, in int3 ChildCoord) + { + int childIndex = ChildCoord.z * 4 + ChildCoord.y * 2 + ChildCoord.x; + int2 textureIndex = GetTextureIndexOfNode(NodeIndex); + textureIndex.x += 1 + childIndex; + return GetNodeFromTexture(textureIndex); + } + + Node GetParentNode(in int NodeIndex) + { + return GetNodeFromTexture(GetTextureIndexOfNode(NodeIndex)); + } + + /** + * Given a UV position [0, 1] within the voxel grid, traverse to the leaf containing it. + * Outputs the node's corresponding data texture index and octree coordinates. + */ + Node TraverseToLeaf(in float3 PositionUV, inout OctreeTraversal Traversal) + { + // Get the size of the node at the current level + float sizeAtLevel = exp2(-1.0 * float(Traversal.Coords.w)); + float3 start = float3(Traversal.Coords.xyz) * sizeAtLevel; + float3 end = start + sizeAtLevel; + + Node child; + + for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) + { + // Find the octree child that contains the given position. + // Example: the point (0.75, 0.25, 0.75) belongs to the child at coords (1, 0, 1) + float3 center = 0.5 * (start + end); + int3 childCoord = step(center, PositionUV); + + // Get the octree coordinates for the next level down. + Traversal.Coords = int4(Traversal.Coords.xyz * 2 + childCoord, Traversal.Coords.w + 1); + + child = GetChildNode(Traversal.Index, childCoord); + + if (child.Flag != OCTREE_FLAG_INTERNAL) + { + // Found leaf - stop traversing + break; + } + + // Keep going! + start = lerp(start, center, childCoord); + end = lerp(center, end, childCoord); + Traversal.Index = child.Data; + } + + return child; + } + + void BeginTraversal(in float3 PositionUV, out OctreeTraversal Traversal, out TileSample Sample) + { + Traversal = (OctreeTraversal) 0; + + // Start from root + Node currentNode = GetNodeFromTexture(int2(0, 0)); + if (currentNode.Flag == OCTREE_FLAG_INTERNAL) + { + currentNode = TraverseToLeaf(PositionUV, Traversal); + } + + Sample = GetSampleFromNode(currentNode, Traversal.Coords); + Sample.LocalUV = clamp(GetTileUV(PositionUV, Sample.Coords), 0.0, 1.0); + } + + void ResumeTraversal(in float3 PositionUV, inout OctreeTraversal Traversal, inout TileSample Sample) + { + if (IsInsideTile(PositionUV, Traversal.Coords)) + { + // Continue to sample the same tile, marching to a different voxel. + Sample.LocalUV = clamp(GetTileUV(PositionUV, Sample.Coords), 0.0, 1.0); + return; + } + + // Otherwise, go up tree until we find a parent tile containing the position. + for (int i = 0; i < OCTREE_MAX_LEVELS; ++i) + { + Traversal.Coords.w -= 1; // Up one level + Traversal.Coords.xyz /= 2; // Get coordinates of the parent tile. + + if (IsInsideTile(PositionUV, Traversal.Coords)) + { + break; + } + Node parent = GetParentNode(Traversal.Index); + Traversal.Index = parent.Data; + } + + // Go down tree + Node node = TraverseToLeaf(PositionUV, Traversal); + Sample = GetSampleFromNode(node, Traversal.Coords); + Sample.LocalUV = clamp(GetTileUV(PositionUV, Sample.Coords), 0.0, 1.0); + } + + /** + * Given UV coordinates within a tile, and the voxel dimensions along a ray + * passing through the coordinates, find the intersections where the ray enters + * and exits the voxel cell. + * + * Outputs the distance to the points and the surface normals in UV Shape Space. + */ + RayIntersections GetVoxelIntersections(in float3 TileUV, in float3 VoxelSizeAlongRay) + { + float3 voxelCoord = TileUV * float3(GridDimensions); + float3 directions = sign(VoxelSizeAlongRay); + float3 positiveDirections = max(directions, 0.0); + float3 entryCoord = lerp(ceil(voxelCoord), floor(voxelCoord), positiveDirections); + float3 exitCoord = entryCoord + directions; + + RayIntersections Intersections; + + float3 distanceFromEntry = -abs((entryCoord - voxelCoord) * VoxelSizeAlongRay); + float lastEntry = MaxComponent(distanceFromEntry); + bool3 isLastEntry = Equal(distanceFromEntry, float3(lastEntry, lastEntry, lastEntry)); + Intersections.Entry.Normal = -1.0 * float3(isLastEntry) * directions; + Intersections.Entry.t = lastEntry; + + float3 distanceToExit = abs((exitCoord - voxelCoord) * VoxelSizeAlongRay); + float firstExit = MinComponent(distanceToExit); + bool3 isFirstExit = Equal(distanceToExit, float3(firstExit, firstExit, firstExit)); + Intersections.Exit.Normal = float3(isFirstExit) * directions; + Intersections.Exit.t = firstExit; + + return Intersections; + } + + /** + * Gets the size of a voxel cell within the given octree level. + */ + float3 GetVoxelSizeAtLevel(in int Level) + { + float3 sampleCount = float(1u << Level) * float3(GridDimensions); + float3 voxelSizeUV = 1.0 / sampleCount; + return GridShape.ScaleUVToShapeUVSpace(voxelSizeUV); + } + + /** + * Computes the next intersection in the octree by stepping to the next voxel cell + * within the shape along the ray. + * + * Outputs the distance relative to the initial intersection, as opposed to the distance from the + * origin of the ray. + */ + Intersection GetNextVoxelIntersection(in TileSample Sample, in float3 Direction, in RayIntersections ShapeIntersections, in float3x3 JacobianT, in float CurrentT) + { + // The Jacobian is computed in a space where the shape spans [-1, 1]. + // But the ray is marched in a space where the shape fills [0, 1]. + // So we need to scale the Jacobian by 2. + float3 gradient = 2.0 * mul(Direction, JacobianT); + float3 voxelSizeAlongRay = GetVoxelSizeAtLevel(Sample.Coords.w) / gradient; + RayIntersections voxelIntersections = GetVoxelIntersections(Sample.LocalUV, voxelSizeAlongRay); + + // Transform normal from UV Shape Space to Cartesian space. + float3 voxelNormal = normalize(mul(JacobianT, voxelIntersections.Entry.Normal)); + // Then, compare with the original shape intersection to choose the appropriate normal. + Intersection voxelEntry = NewIntersection(CurrentT + voxelIntersections.Entry.t, voxelNormal); + Intersection entry = Max(ShapeIntersections.Entry, voxelEntry); + + float fixedStep = MinComponent(abs(voxelSizeAlongRay)); + float shift = fixedStep * SHIFT_EPSILON; + float dt = voxelIntersections.Exit.t + shift; + if ((CurrentT + dt) > ShapeIntersections.Exit.t) + { + // Stop at end of shape. + dt = ShapeIntersections.Exit.t - CurrentT + shift; + } + float stepSize = clamp(dt, fixedStep * MINIMUM_STEP_SCALAR, fixedStep + shift); + + return NewIntersection(stepSize, entry.Normal); + } +}; diff --git a/Shaders/Private/CesiumVoxelTemplate.usf b/Shaders/Private/CesiumVoxelTemplate.usf new file mode 100644 index 000000000..7c7fa3477 --- /dev/null +++ b/Shaders/Private/CesiumVoxelTemplate.usf @@ -0,0 +1,221 @@ +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumVoxelTemplate.usf: Template for creating custom shaders to style voxel data. +=============================================================================*/ + +/*======================= + BEGIN CUSTOM SHADER +=========================*/ + +struct CustomShaderProperties +{ +%s +}; + +struct CustomShader +{ +%s + + float4 Shade(CustomShaderProperties Properties) + { +%s + } +}; + +/*======================= + END CUSTOM SHADER +=========================*/ + +/*=========================== + BEGIN VOXEL MEGATEXTURES +=============================*/ + +struct VoxelMegatextures +{ + %s + int ShapeConstant; + uint3 TileCount; // Number of tiles in the texture, in three dimensions. + + // NOTE: Unlike VoxelOctree, these dimensions are specified with respect to the voxel attributes + // in the glTF model. For box and cylinder voxels, the YZ dimensions will be swapped. + uint3 GridDimensions; + uint3 PaddingBefore; + uint3 PaddingAfter; + + int3 TileIndexToCoords(in int Index) + { + if (TileCount.x == 0 || TileCount.y == 0 || TileCount.z == 0) + { + return 0; + } + int ZSlice = TileCount.x * TileCount.y; + int Z = Index / ZSlice; + int Y = (Index % ZSlice) / TileCount.x; + int X = Index % TileCount.x; + return int3(X, Y, Z) * (GridDimensions + PaddingBefore + PaddingAfter); + } + + CustomShaderProperties GetProperties(in TileSample Sample) + { + // Compute the tile location within the texture. + float3 TileCoords = TileIndexToCoords(Sample.Index); + + // Compute int coordinates of the voxel within the tile. + float3 LocalUV = Sample.LocalUV; + uint3 DataDimensions = GridDimensions + PaddingBefore + PaddingAfter; + + 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); + } + + 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 + + return Properties; + } +}; + +/*=========================== + END VOXEL MEGATEXTURES +=============================*/ + +/*=========================== + MAIN FUNCTION BODY +=============================*/ + +#define STEP_COUNT_MAX 1000 +#define ALPHA_ACCUMULATION_MAX 0.98 // Must be > 0.0 and <= 1.0 + +VoxelOctree Octree; +Octree.GridShape = (Shape) 0; +Octree.GridShape.Initialize(ShapeConstant); + +Ray R = (Ray) 0; +R.Origin = RayOrigin; +R.Direction = RayDirection; + +// Input ray is Unit Space. +// +// Ultimately, we want to traverse the voxel grid in a [0, 1] UV space to simplify much of +// the voxel octree math. This is simply referred to UV space, or "vanilla" UV space. +// However, this UV space won't always be a perfect voxel cube. It must conform to the specified +// shape of the voxel volume. Voxel cells may be curved (e.g., around a cylinder), or non-uniformly +// scaled. This shape-relative UV space is aptly referred to as Shape UV Space. +// +// Shape UV Space is DIFFERENT from the shape's Unit Space. Think of Unit Space as the perfect +// version of the grid geometry: a solid box, cylinder, or ellipsoid, within [-1, 1]. +// The actual voxel volume can be a subsection of that perfect unit space, e.g., a hollowed +// cylinder with radius [0.5, 1]. +// +// 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, +// 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. +// Spaces will be referred to as follows: +// +// Unit Space: Unit space of the grid geometry from [-1, 1]. A perfectly solid box, cylinder, or ellipsoid. +// Shape UV Space: Voxel space from [0, 1] conforming to the actual voxel volume. The volume could be a box, a part of a cylinder, or a region on an ellipsoid. +// (Vanilla) UV Space: Voxel space within an untransformed voxel octree. This can be envisioned as a simple cube spanning [0, 1] in three dimensions. + +RayIntersections Intersections = Octree.GridShape.Intersect(R); +if (Intersections.Entry.t == NO_HIT) { + return 0; +} + +// Intersections are returned in Unit Space. Transform to UV space [0, 1] for raymarching through octree. +R.Origin = UnitToUV(R.Origin); +R.Direction = R.Direction * 0.5; + +// Initialize octree +Octree.SetNodeData(OctreeData); +Octree.GridDimensions = GridDimensions; + +// Initialize data textures +VoxelMegatextures DataTextures; +DataTextures.ShapeConstant = ShapeConstant; +DataTextures.TileCount = TileCount; + +// Account for y-up -> z-up conventions for certain shapes. +switch (ShapeConstant) { + case BOX: + DataTextures.GridDimensions = round(GridDimensions.xzy); + DataTextures.PaddingBefore = round(PaddingBefore.xzy); + DataTextures.PaddingAfter = round(PaddingAfter.xzy); + break; + default: + DataTextures.GridDimensions = round(GridDimensions); + DataTextures.PaddingBefore = round(PaddingBefore); + DataTextures.PaddingAfter = round(PaddingAfter); + break; +} + +%s + +float CurrentT = Intersections.Entry.t; +float EndT = Intersections.Exit.t; +float3 PositionUV = R.Origin + CurrentT * R.Direction; + +// The Jacobian is necessary to compute voxel intersections with respect to the grid's shape. +float3x3 JacobianT; +float3 PositionShapeUVSpace = Octree.GridShape.ConvertUVToShapeUVSpace(PositionUV, JacobianT); + +float3 RawDirection = R.Direction; + +OctreeTraversal Traversal; +TileSample Sample; +Octree.BeginTraversal(PositionShapeUVSpace, Traversal, Sample); +Intersection NextIntersection = Octree.GetNextVoxelIntersection(Sample, RawDirection, Intersections, JacobianT, CurrentT); + +float4 AccumulatedResult = float4(0, 0, 0, 0); + +CustomShaderProperties Properties; +CustomShader CS; + +for (int step = 0; step < STEP_COUNT_MAX; step++) { + if (Sample.Index >= 0) { + Properties = DataTextures.GetProperties(Sample); + // TODO: expose additional properties? + float4 result = CS.Shade(Properties); + AccumulatedResult += result; + + // Stop traversing if the alpha has been fully saturated. + if (AccumulatedResult.w > ALPHA_ACCUMULATION_MAX) { + AccumulatedResult.w = ALPHA_ACCUMULATION_MAX; + break; + } + } + + if (NextIntersection.t <= 0.0) { + // Shape is infinitely thin. The ray may have hit the edge of a + // foreground voxel. Step ahead slightly to check for more voxels + NextIntersection.t = 0.00001; + } + + // Keep raymarching + CurrentT += NextIntersection.t; + if (CurrentT > EndT) { + break; + } + + 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); +} + +// Convert the alpha from [0,ALPHA_ACCUMULATION_MAX] to [0,1] +AccumulatedResult.a /= ALPHA_ACCUMULATION_MAX; + +return AccumulatedResult; diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index 1ce7d4387..14b85a08f 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -1,4 +1,4 @@ -// Copyright 2020-2024 CesiumGS, Inc. and Contributors +// Copyright 2020-2025 CesiumGS, Inc. and Contributors #include "CesiumVoxelMetadataComponent.h" @@ -12,6 +12,7 @@ #include "EncodedMetadataConversions.h" #include "GenerateMaterialUtility.h" #include "Misc/FileHelper.h" +#include "ShaderCore.h" #include "UnrealMetadataConversions.h" #include @@ -229,24 +230,24 @@ struct VoxelMetadataClassification : public MaterialNodeClassification { }; struct MaterialResourceLibrary { - FString HlslShaderTemplate; + FString ShaderTemplate; UMaterialFunctionMaterialLayer* MaterialLayerTemplate; TObjectPtr pDefaultVolumeTexture; MaterialResourceLibrary() { - static FString ContentDir = IPluginManager::Get() - .FindPlugin(TEXT("CesiumForUnreal")) - ->GetContentDir(); - FFileHelper::LoadFileToString( - HlslShaderTemplate, - *(ContentDir / "Materials/CesiumVoxelTemplate.hlsl")); + FString Path = GetShaderSourceFilePath( + "/Plugin/CesiumForUnreal/Private/CesiumVoxelTemplate.usf"); + + if (!Path.IsEmpty()) { + FFileHelper::LoadFileToString(ShaderTemplate, *Path); + } MaterialLayerTemplate = LoadObjFromPath( "/CesiumForUnreal/Materials/Layers/ML_CesiumVoxel"); } bool isValid() const { - return !HlslShaderTemplate.IsEmpty() && MaterialLayerTemplate && + return !ShaderTemplate.IsEmpty() && MaterialLayerTemplate && pDefaultVolumeTexture; } }; @@ -768,9 +769,9 @@ static void GenerateMaterialNodes( NodeY = DataSectionY; // Inspired by HLSLMaterialTranslator.cpp. Similar to MaterialTemplate.ush, - // CesiumVoxelTemplate.hlsl contains "%s" formatters that will be replaced + // CesiumVoxelTemplate.usf contains "%s" formatters that will be replaced // with generated code. - FLazyPrintf LazyPrintf(*ResourceLibrary.HlslShaderTemplate); + FLazyPrintf LazyPrintf(*ResourceLibrary.ShaderTemplate); CustomShaderBuilder Builder; const TArray& Properties = From 5aa80eb8742beb1c910aad7b5048127c0c3b02c1 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 9 Jul 2025 12:07:57 -0400 Subject: [PATCH 27/34] Improve shader documention [skip ci] --- Shaders/Private/CesiumBox.usf | 2 +- Shaders/Private/CesiumShape.usf | 2 +- Shaders/Private/CesiumShapeConstants.usf | 6 ++++++ Shaders/Private/CesiumVectorUtility.usf | 4 ++-- Shaders/Private/CesiumVoxelTemplate.usf | 3 +++ 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Shaders/Private/CesiumBox.usf b/Shaders/Private/CesiumBox.usf index 51eeec486..238ba13bc 100644 --- a/Shaders/Private/CesiumBox.usf +++ b/Shaders/Private/CesiumBox.usf @@ -3,7 +3,7 @@ // Copyright 2020-2025 CesiumGS, Inc. and Contributors /*============================================================================= - CesiumBoxShape.usf: Utility for intersecting a ray with a box shape. + CesiumBox.usf: An implicit box shape that may be intersected by a ray. =============================================================================*/ #include "CesiumRayIntersection.usf" diff --git a/Shaders/Private/CesiumShape.usf b/Shaders/Private/CesiumShape.usf index 8c717de56..02d60d887 100644 --- a/Shaders/Private/CesiumShape.usf +++ b/Shaders/Private/CesiumShape.usf @@ -3,7 +3,7 @@ // Copyright 2020-2025 CesiumGS, Inc. and Contributors /*============================================================================= - CesiumShape.usf: Utility for intersecting a ray with implicit shapes. + CesiumShape.usf: An implicit shape that can be intersected by a ray. =============================================================================*/ #include "CesiumShapeConstants.usf" diff --git a/Shaders/Private/CesiumShapeConstants.usf b/Shaders/Private/CesiumShapeConstants.usf index 5206a9c6d..41a03a6fc 100644 --- a/Shaders/Private/CesiumShapeConstants.usf +++ b/Shaders/Private/CesiumShapeConstants.usf @@ -1,5 +1,11 @@ #pragma once +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumShapeConstants.usf: Constants for supported shape types. (See CesiumShape.usf) +=============================================================================*/ + #define BOX 1 #define CYLINDER 2 #define ELLIPSOID 3 diff --git a/Shaders/Private/CesiumVectorUtility.usf b/Shaders/Private/CesiumVectorUtility.usf index a9f2b59c4..5fb160b4f 100644 --- a/Shaders/Private/CesiumVectorUtility.usf +++ b/Shaders/Private/CesiumVectorUtility.usf @@ -1,7 +1,7 @@ -// Copyright 2020-2025 CesiumGS, Inc. and Contributors - #pragma once +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + /*=========================== CesiumVectorUtility.usf: General utility for handling vectors (e.g., float3s). =============================*/ diff --git a/Shaders/Private/CesiumVoxelTemplate.usf b/Shaders/Private/CesiumVoxelTemplate.usf index 7c7fa3477..d28fe9613 100644 --- a/Shaders/Private/CesiumVoxelTemplate.usf +++ b/Shaders/Private/CesiumVoxelTemplate.usf @@ -4,6 +4,9 @@ CesiumVoxelTemplate.usf: Template for creating custom shaders to style voxel data. =============================================================================*/ +// This depends on CesiumVoxelOctree.usf, but a local #include cannot be resolved here. +// It must be linked under "Include File Paths" in a Custom node in the material. + /*======================= BEGIN CUSTOM SHADER =========================*/ From bc36cde119a476c74f74b8bc6ae2c72f4a9b50fc Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 9 Jul 2025 15:56:42 -0400 Subject: [PATCH 28/34] Attempt to fix CI --- .../CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp | 4 +--- Source/CesiumRuntime/Private/VoxelMegatextures.cpp | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp b/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp index 5ca07e4c0..e5c8a5cda 100644 --- a/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp +++ b/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp @@ -388,9 +388,7 @@ int64 FCesiumPropertyAttributeProperty::getAccessorStride() const { this->_property, this->_valueType, this->_normalized, - [](const auto& view) -> int64 { - return view.accessorView().stride(); - }); + [](const auto& view) -> int64 { return view.accessorView().stride(); }); } const std::byte* FCesiumPropertyAttributeProperty::getAccessorData() const { diff --git a/Source/CesiumRuntime/Private/VoxelMegatextures.cpp b/Source/CesiumRuntime/Private/VoxelMegatextures.cpp index fb59b34b3..5a0d758ed 100644 --- a/Source/CesiumRuntime/Private/VoxelMegatextures.cpp +++ b/Source/CesiumRuntime/Private/VoxelMegatextures.cpp @@ -13,6 +13,7 @@ #include "EncodedMetadataConversions.h" #include "Engine/VolumeTexture.h" #include "Templates/UniquePtr.h" +#include "UObject/Package.h" #include #include From ebb5282d32b23ff3785a1f3db8d25fc60e93ce9a Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Wed, 9 Jul 2025 16:46:53 -0400 Subject: [PATCH 29/34] Fix CI errors for non-Windows --- .../CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp | 3 ++- .../CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp | 4 ++-- Source/CesiumRuntime/Private/VoxelMegatextures.h | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index 14b85a08f..41e4cd230 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -313,7 +313,8 @@ struct CustomShaderBuilder { FString DefaultValueName = PropertyName + MaterialPropertyDefaultValueSuffix; DeclareShaderProperties += - "\n\t" + isNormalizedProperty ? normalizedHlslType : encodedHlslType; + "\n\t" + + (isNormalizedProperty ? normalizedHlslType : encodedHlslType); DeclareShaderProperties += " " + DefaultValueName + ";"; } } diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp index 0f725c87d..a15b28ccd 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -240,7 +240,7 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( } if (pDescription && pVoxelClass) { - for (const auto propertyIt : pVoxelClass->properties) { + for (const auto& propertyIt : pVoxelClass->properties) { FString UnrealName(propertyIt.first.c_str()); for (const FCesiumPropertyAttributePropertyDescription& Property : @@ -451,7 +451,7 @@ UCesiumVoxelRendererComponent::CreateVoxelMaterial( const Cesium3DTiles::MetadataEntity& metadata = *tilesetMetadata.metadata; const Cesium3DTiles::Class& tilesetClass = tilesetMetadata.schema->classes.at(metadata.classProperty); - for (const auto propertyIt : tilesetClass.properties) { + for (const auto& propertyIt : tilesetClass.properties) { if (propertyIt.second.semantic == "TILESET_TILE_COUNT") { const auto tileCountIt = metadata.properties.find(propertyIt.first); if (tileCountIt != metadata.properties.end()) { diff --git a/Source/CesiumRuntime/Private/VoxelMegatextures.h b/Source/CesiumRuntime/Private/VoxelMegatextures.h index c492bf673..dfcf0ab80 100644 --- a/Source/CesiumRuntime/Private/VoxelMegatextures.h +++ b/Source/CesiumRuntime/Private/VoxelMegatextures.h @@ -80,7 +80,7 @@ class FVoxelMegatextures { * * @returns The index of the reserved slot, or -1 if none were available. */ - int64_t add(const UCesiumGltfVoxelComponent& voxelComponent); + int64 add(const UCesiumGltfVoxelComponent& voxelComponent); /** * @brief Releases the slot at the specified index, making the space available @@ -119,7 +119,7 @@ class FVoxelMegatextures { * with maximum tile capacity. */ struct Slot { - int64_t index = -1; + int64 index = -1; Slot* pNext = nullptr; Slot* pPrevious = nullptr; std::optional fence; @@ -170,7 +170,7 @@ class FVoxelMegatextures { * * @returns The index of the reserved slot, or -1 if none were available. */ - int64_t reserveNextSlot(); + int64 reserveNextSlot(); std::vector _slots; std::unordered_set _loadingSlots; From 1d31423d6c1caf11878e051412d88dfbb46a5218 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 10 Jul 2025 09:57:54 -0400 Subject: [PATCH 30/34] More CI appeasement --- .../Private/CesiumVoxelMetadataComponent.cpp | 11 +++++++---- .../Private/CesiumVoxelRendererComponent.h | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index 41e4cd230..635290b97 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -381,7 +381,8 @@ struct CustomShaderBuilder { // Declare the value transforms underneath the corresponding data texture // variable. e.g., float myProperty_SCALE; DeclareDataTextureVariables += - "\n\t" + isNormalizedProperty ? normalizedHlslType : encodedHlslType; + "\n\t" + + (isNormalizedProperty ? normalizedHlslType : encodedHlslType); DeclareDataTextureVariables += " " + ScaleName + ";"; SetDataTextures += "\nDataTextures." + ScaleName + " = " + ScaleName + ";"; @@ -393,7 +394,8 @@ struct CustomShaderBuilder { if (Property.PropertyDetails.bHasOffset) { FString OffsetName = PropertyName + MaterialPropertyOffsetSuffix; DeclareDataTextureVariables += - "\n\t" + isNormalizedProperty ? normalizedHlslType : encodedHlslType; + "\n\t" + + (isNormalizedProperty ? normalizedHlslType : encodedHlslType); DeclareDataTextureVariables += " " + OffsetName + ";"; SetDataTextures += "\nDataTextures." + OffsetName + " = " + OffsetName + ";"; @@ -419,7 +421,8 @@ struct CustomShaderBuilder { FString DefaultValueName = PropertyName + MaterialPropertyDefaultValueSuffix; DeclareDataTextureVariables += - "\n\t" + isNormalizedProperty ? normalizedHlslType : encodedHlslType; + "\n\t" + + (isNormalizedProperty ? normalizedHlslType : encodedHlslType); DeclareDataTextureVariables += " " + DefaultValueName + ";"; SetDataTextures += "\nDataTextures." + DefaultValueName + " = " + DefaultValueName + ";"; @@ -513,7 +516,7 @@ ClassifyNodes(UMaterialFunctionMaterialLayer* Layer) { UMaterialExpressionMaterialFunctionCall* pFunctionCall = Cast(pNode); const FString& FunctionName = - pFunctionCall && pFunctionCall->MaterialFunction + (pFunctionCall && pFunctionCall->MaterialFunction) ? pFunctionCall->MaterialFunction->GetName() : FString(); if (FunctionName.Contains("BreakOutFloat4")) { diff --git a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h index 411797df1..5d078b04d 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h @@ -9,8 +9,8 @@ #include "CustomDepthParameters.h" #include "Materials/MaterialInstanceDynamic.h" #include "Templates/UniquePtr.h" -#include "VoxelMegatextures.h" #include "VoxelGridShape.h" +#include "VoxelMegatextures.h" #include "VoxelOctree.h" #include From dd65adee68dc156812d53ef40abd108ce44c7e71 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Thu, 10 Jul 2025 16:27:37 -0400 Subject: [PATCH 31/34] Formatting and packaging fix --- .../Private/CesiumVoxelMetadataComponent.cpp | 3 +-- Source/CesiumRuntime/Public/Cesium3DTileset.h | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index 635290b97..3dd5cda33 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -55,8 +55,7 @@ using namespace GenerateMaterialUtility; static const FString RaymarchDescription = "Voxel Raymarch"; -UCesiumVoxelMetadataComponent::UCesiumVoxelMetadataComponent() - : UActorComponent() { +UCesiumVoxelMetadataComponent::UCesiumVoxelMetadataComponent() { // Structure to hold one-time initialization struct FConstructorStatics { ConstructorHelpers::FObjectFinder DefaultVolumeTexture; diff --git a/Source/CesiumRuntime/Public/Cesium3DTileset.h b/Source/CesiumRuntime/Public/Cesium3DTileset.h index 3275fe18f..0382d7e83 100644 --- a/Source/CesiumRuntime/Public/Cesium3DTileset.h +++ b/Source/CesiumRuntime/Public/Cesium3DTileset.h @@ -1273,11 +1273,12 @@ class CESIUMRUNTIME_API ACesium3DTileset : public AActor { UCesiumEllipsoid* NewEllpisoid); /** - * Creates and attaches a \ref UCesiumVoxelRendererComponent for rendering voxel data. + * Creates and attaches a \ref UCesiumVoxelRendererComponent for rendering + * voxel data. */ - void createVoxelRenderer( - const Cesium3DTiles::ExtensionContent3dTilesContentVoxels& - VoxelExtension); + void + createVoxelRenderer(const Cesium3DTiles::ExtensionContent3dTilesContentVoxels& + VoxelExtension); /** * Writes the values of all properties of this actor into the From deef0cea63e2dd045e343e2a1c96b6c6c8c9436f Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 14 Jul 2025 11:35:49 -0400 Subject: [PATCH 32/34] Fixes to voxel metadata component --- .../Private/CesiumVoxelMetadataComponent.cpp | 23 ++++++++----------- .../Public/CesiumVoxelMetadataComponent.h | 2 -- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp index 3dd5cda33..a91b9b350 100644 --- a/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -15,6 +15,7 @@ #include "ShaderCore.h" #include "UnrealMetadataConversions.h" +#include #include #if WITH_EDITOR @@ -47,8 +48,7 @@ #include "Modules/ModuleManager.h" #include "Subsystems/AssetEditorSubsystem.h" #include "UObject/Package.h" - -#include +#endif using namespace EncodedFeaturesMetadata; using namespace GenerateMaterialUtility; @@ -128,15 +128,13 @@ GetValueTypeFromClassProperty(const Cesium3DTiles::ClassProperty& Property) { } void AutoFillVoxelClassDescription( - FCesiumVoxelClassDescription& Description, - const Cesium3DTiles::Schema& TilesetSchema, - const std::string& VoxelClassID) { - Description.ID = TilesetSchema.id.c_str(); + FCesiumVoxelClassDescription& description, + const std::string& voxelClassID, + const Cesium3DTiles::Class& voxelClass) { + description.ID = voxelClassID.c_str(); - const Cesium3DTiles::Class& voxelClass = - TilesetSchema.classes.at(VoxelClassID); for (const auto& propertyIt : voxelClass.properties) { - auto pExistingProperty = Description.Properties.FindByPredicate( + auto pExistingProperty = description.Properties.FindByPredicate( [&propertyName = propertyIt.first]( const FCesiumPropertyAttributePropertyDescription& existingProperty) { @@ -149,7 +147,7 @@ void AutoFillVoxelClassDescription( } FCesiumPropertyAttributePropertyDescription& property = - Description.Properties.Emplace_GetRef(); + description.Properties.Emplace_GetRef(); property.Name = propertyIt.first.c_str(); property.PropertyDetails.SetValueType( @@ -214,8 +212,8 @@ void UCesiumVoxelMetadataComponent::AutoFill() { AutoFillVoxelClassDescription( this->Description, - *pMetadata->schema, - voxelClassId); + voxelClassId, + pMetadata->schema->classes.at(voxelClassId)); Super::PostEditChange(); @@ -456,7 +454,6 @@ struct CustomShaderBuilder { "\tfloat4 Shade(CustomShaderProperties Properties) {\n" "%s\n" "\t}\n}"; -#endif void UCesiumVoxelMetadataComponent::UpdateShaderPreview() { // Inspired by HLSLMaterialTranslator.cpp. Similar to MaterialTemplate.ush, diff --git a/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h index 800c2b5b3..0adb52587 100644 --- a/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h +++ b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h @@ -11,8 +11,6 @@ #include "CesiumVoxelMetadataComponent.generated.h" -class UVolumeTexture; - /** * @brief Description of the metadata properties available in the class used by * the 3DTILES_content_voxels extension. Exposes what properties are available From cb3a77fac5e8980bcdc84bf1b891c92449612794 Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Mon, 14 Jul 2025 12:23:32 -0400 Subject: [PATCH 33/34] More voxel component fixes --- Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h index 0adb52587..d62984127 100644 --- a/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h +++ b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h @@ -148,10 +148,10 @@ class CESIUMRUNTIME_API UCesiumVoxelMetadataComponent : public UActorComponent { #endif private: -#if WITH_EDITOR TObjectPtr pDefaultVolumeTexture; - static const FString ShaderPreviewTemplate; +#if WITH_EDITOR + static const FString ShaderPreviewTemplate; void UpdateShaderPreview(); #endif }; From 46f9c4b0079105fa12661a9a79786c3f6c67fd9e Mon Sep 17 00:00:00 2001 From: Janine Liu Date: Fri, 8 Aug 2025 16:34:10 -0400 Subject: [PATCH 34/34] Add missing header --- Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h index 8d9bacaa3..bc8ada27c 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h +++ b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h @@ -3,6 +3,7 @@ #pragma once #include "CesiumPrimitiveMetadata.h" +#include "Components/SceneComponent.h" #include "CoreMinimal.h" #include