diff --git a/CHANGES.md b/CHANGES.md index f5324fb74..cede034ab 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.19.1 - 2025-09-02 ##### Fixes :wrench: diff --git a/Content/Materials/Instances/MI_CesiumVoxel.uasset b/Content/Materials/Instances/MI_CesiumVoxel.uasset new file mode 100644 index 000000000..a36598ac2 Binary files /dev/null and b/Content/Materials/Instances/MI_CesiumVoxel.uasset differ diff --git a/Content/Materials/Layers/ML_CesiumVoxel.uasset b/Content/Materials/Layers/ML_CesiumVoxel.uasset new file mode 100644 index 000000000..811ed7395 Binary files /dev/null and b/Content/Materials/Layers/ML_CesiumVoxel.uasset differ diff --git a/Shaders/Private/CesiumBox.usf b/Shaders/Private/CesiumBox.usf new file mode 100644 index 000000000..238ba13bc --- /dev/null +++ b/Shaders/Private/CesiumBox.usf @@ -0,0 +1,68 @@ +#pragma once + +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumBox.usf: An implicit box shape that may be intersected by a ray. +=============================================================================*/ + +#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..02d60d887 --- /dev/null +++ b/Shaders/Private/CesiumShape.usf @@ -0,0 +1,98 @@ +#pragma once + +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + CesiumShape.usf: An implicit shape that can be intersected by a ray. +=============================================================================*/ + +#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..41a03a6fc --- /dev/null +++ b/Shaders/Private/CesiumShapeConstants.usf @@ -0,0 +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 new file mode 100644 index 000000000..5fb160b4f --- /dev/null +++ b/Shaders/Private/CesiumVectorUtility.usf @@ -0,0 +1,36 @@ +#pragma once + +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*=========================== + 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..d28fe9613 --- /dev/null +++ b/Shaders/Private/CesiumVoxelTemplate.usf @@ -0,0 +1,224 @@ +// Copyright 2020-2025 CesiumGS, Inc. and Contributors + +/*============================================================================= + 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 +=========================*/ + +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/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 07554d883..4af26ff3d 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 @@ -366,6 +372,8 @@ void ACesium3DTileset::PostInitProperties() { } } +#pragma region Getters / Setters + void ACesium3DTileset::SetUseLodTransitions(bool InUseLodTransitions) { if (InUseLodTransitions != this->UseLodTransitions) { this->UseLodTransitions = InUseLodTransitions; @@ -577,6 +585,8 @@ void ACesium3DTileset::SetTranslucencySortPriority( } } +#pragma endregion + void ACesium3DTileset::PlayMovieSequencer() { this->_beforeMoviePreloadAncestors = this->PreloadAncestors; this->_beforeMoviePreloadSiblings = this->PreloadSiblings; @@ -740,6 +750,10 @@ void ACesium3DTileset::UpdateTransformFromCesium() { this->BoundingVolumePoolComponent->UpdateTransformFromCesium( CesiumToUnreal); } + + if (this->_pVoxelRendererComponent) { + this->_pVoxelRendererComponent->UpdateTransformFromCesium(CesiumToUnreal); + } } void ACesium3DTileset::HandleOnGeoreferenceEllipsoidChanged( @@ -934,7 +948,6 @@ void ACesium3DTileset::LoadTileset() { // Check if this component exists for backwards compatibility. PRAGMA_DISABLE_DEPRECATION_WARNINGS - const UDEPRECATED_CesiumEncodedMetadataComponent* pEncodedMetadataComponent = this->FindComponentByClass(); @@ -954,9 +967,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() @@ -1153,6 +1171,24 @@ void ACesium3DTileset::LoadTileset() { TCHAR_TO_UTF8(*dbFile)); #endif + this->_pTileset->getRootTileAvailableEvent().thenImmediately([thiz = this]() { + 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->createVoxelRenderer(*pVoxelExtension); + } + }); + for (UCesiumRasterOverlay* pOverlay : rasterOverlays) { if (pOverlay->IsActive()) { pOverlay->AddToTileset(); @@ -1246,9 +1282,14 @@ void ACesium3DTileset::DestroyTileset() { } } + if (this->_pVoxelRendererComponent) { + CesiumLifetime::destroyComponentRecursively(this->_pVoxelRendererComponent); + this->_pVoxelRendererComponent = nullptr; + } + // 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) { @@ -1316,6 +1357,8 @@ std::vector ACesium3DTileset::GetCameras() const { return cameras; } +#pragma region Camera Collections + std::vector ACesium3DTileset::GetPlayerCameras() const { UWorld* pWorld = this->GetWorld(); if (!pWorld) { @@ -1650,6 +1693,8 @@ std::vector ACesium3DTileset::GetEditorCameras() const { } #endif +#pragma endregion + bool ACesium3DTileset::ShouldTickIfViewportsOnly() const { return this->UpdateInEditor; } @@ -1747,12 +1792,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, @@ -2073,31 +2117,37 @@ void ACesium3DTileset::Tick(float DeltaTime) { updateLastViewUpdateResultState(*pResult); - removeCollisionForTiles(pResult->tilesFadingOut); - removeVisibleTilesFromList( this->_tilesToHideNextFrame, pResult->tilesToRenderThisFrame); - hideTiles(this->_tilesToHideNextFrame); - _tilesToHideNextFrame.clear(); - for (const Cesium3DTilesSelection::Tile::ConstPointer& pTile : - pResult->tilesFadingOut) { - const 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(this->_tilesToHideNextFrame); + + _tilesToHideNextFrame.clear(); + for (const Cesium3DTilesSelection::Tile::ConstPointer& pTile : + pResult->tilesFadingOut) { + const 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(); @@ -2228,8 +2278,9 @@ void ACesium3DTileset::PostEditChangeProperty( pTileExcluder->Refresh(); } - // Maximum Screen Space Error can affect how attenuated points are rendered, - // so propagate the new value to the render proxies for this tileset. + // Maximum Screen Space Error can affect how attenuated points are + // rendered, so propagate the new value to the render proxies for this + // tileset. FCesiumGltfPointsSceneProxyUpdater::UpdateSettingsInProxies(this); } } @@ -2315,3 +2366,50 @@ void ACesium3DTileset::RuntimeSettingsChanged( } } #endif + +void ACesium3DTileset::createVoxelRenderer( + const Cesium3DTiles::ExtensionContent3dTilesContentVoxels& VoxelExtension) { + 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 = + 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 FCesiumVoxelClassDescription* pVoxelClassDescription = + this->_voxelClassDescription ? &(*this->_voxelClassDescription) : nullptr; + + 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/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index 4df6f8457..2b651d5ea 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 @@ -90,6 +92,8 @@ class HalfConstructedReal : public UCesiumGltfComponent::HalfConstructed { }; } // namespace +const int32_t VoxelPrimitiveMode = 2147483647; + template struct IsAccessorView; template struct IsAccessorView : std::false_type {}; @@ -1879,6 +1883,144 @@ 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 && !tilesetPadding) { + return true; + } + + if (gltfPadding.has_value() != tilesetPadding.has_value()) { + return false; + } + + 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 != VoxelPrimitiveMode) { + 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::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(); + + TArray propertyAttributes = + UCesiumPrimitiveMetadataBlueprintLibrary::GetPropertyAttributes( + primitiveResult.Metadata); + + FString classAsFString(pTilesetExtension->classProperty.c_str()); + + 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; + } + } +} + static void loadPrimitive( LoadedPrimitiveResult& result, const glm::dmat4x4& transform, @@ -1892,6 +2034,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(CesiumGltf::VertexAttributeSemantics::POSITION); if (positionAccessorIt == primitive.attributes.end()) { @@ -1954,7 +2108,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) { + if (!primitiveResult.RenderData && + primitiveResult.voxelPropertyAttributeIndex < 0) { result->primitiveResults.pop_back(); } } @@ -3032,12 +3187,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) { @@ -3417,6 +3572,54 @@ static void loadPrimitiveGameThreadPart( } } +static void loadVoxelsGameThreadPart( + CesiumGltf::Model& model, + UCesiumGltfComponent* pGltf, + LoadedPrimitiveResult& loadResult, + const Cesium3DTilesSelection::Tile& tile, + ACesium3DTileset* pTilesetActor) { + TArray attributes = + UCesiumPrimitiveMetadataBlueprintLibrary::GetPropertyAttributes( + loadResult.Metadata); + + if (attributes.IsEmpty()) { + UE_LOG( + LogCesium, + Warning, + TEXT("Voxel primitive has no valid property attributes; skipped.")); + return; + } + + const CesiumGeometry::OctreeTileID* tileId = + std::get_if(&tile.getTileID()); + if (!tileId) { + 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 + + UCesiumGltfVoxelComponent* pVoxel = + NewObject(pGltf, componentName); + + pVoxel->TileId = *tileId; + pVoxel->PropertyAttribute = std::move(attributes[index]); + + pVoxel->SetMobility(pGltf->Mobility); + pVoxel->SetupAttachment(pGltf); + + { + TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::RegisterComponent) + pVoxel->RegisterComponent(); + } +} + /*static*/ CesiumAsync::Future UCesiumGltfComponent::CreateOffGameThread( const CesiumAsync::AsyncSystem& AsyncSystem, @@ -3452,56 +3655,66 @@ 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) { - loadPrimitiveGameThreadPart( - model, - Gltf, - primitive, - cesiumToUnrealTransform, - tile, - createNavCollision, - pTilesetActor, - node.InstanceTransforms, - node.pInstanceFeatures); + if (primitive.voxelPropertyAttributeIndex) { + loadVoxelsGameThreadPart( + model, + pGltf, + primitive, + tile, + pTilesetActor); + } else { + loadPrimitiveGameThreadPart( + model, + pGltf, + primitive, + cesiumToUnrealTransform, + tile, + createNavCollision, + pTilesetActor, + node.InstanceTransforms, + node.pInstanceFeatures); + } } } } - 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 new file mode 100644 index 000000000..4f4ec1d85 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.cpp @@ -0,0 +1,12 @@ +// 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() { Super::BeginDestroy(); } diff --git a/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h new file mode 100644 index 000000000..bc8ada27c --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfVoxelComponent.h @@ -0,0 +1,35 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumPrimitiveMetadata.h" +#include "Components/SceneComponent.h" +#include "CoreMinimal.h" + +#include +#include + +#include "CesiumGltfVoxelComponent.generated.h" + +/** + * A minimal component representing a glTF voxel primitive. + * + * 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 { + GENERATED_BODY() + +public: + // Sets default values for this component's properties + UCesiumGltfVoxelComponent(); + virtual ~UCesiumGltfVoxelComponent(); + + void BeginDestroy(); + + CesiumGeometry::OctreeTileID TileId; + FCesiumPropertyAttribute PropertyAttribute; +}; 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/CesiumPropertyAttributeProperty.cpp b/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp index 2c394f0c7..e5c8a5cda 100644 --- a/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp +++ b/Source/CesiumRuntime/Private/CesiumPropertyAttributeProperty.cpp @@ -3,6 +3,7 @@ #include "CesiumPropertyAttributeProperty.h" #include "CesiumMetadataEnum.h" #include "UnrealMetadataConversions.h" + #include #include #include @@ -382,6 +383,24 @@ 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/CesiumTextureResource.cpp b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp index bcdaef9cd..cae3ced84 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: @@ -415,6 +437,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; @@ -752,3 +797,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/CesiumVoxelMetadataComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp new file mode 100644 index 000000000..a91b9b350 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumVoxelMetadataComponent.cpp @@ -0,0 +1,1074 @@ +// Copyright 2020-2025 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 "ShaderCore.h" +#include "UnrealMetadataConversions.h" + +#include +#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" +#endif + +using namespace EncodedFeaturesMetadata; +using namespace GenerateMaterialUtility; + +static const FString RaymarchDescription = "Voxel Raymarch"; + +UCesiumVoxelMetadataComponent::UCesiumVoxelMetadataComponent() { + // Structure to hold one-time initialization + struct FConstructorStatics { + ConstructorHelpers::FObjectFinder DefaultVolumeTexture; + FConstructorStatics() + : DefaultVolumeTexture( + TEXT("/Engine/EngineResources/DefaultVolumeTexture")) {} + }; + static FConstructorStatics ConstructorStatics; + pDefaultVolumeTexture = 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) { + description.ID = voxelClassID.c_str(); + + 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 || !pTileset->getRootTile()) { + return; + } + + const Cesium3DTilesSelection::TileExternalContent* pExternalContent = + pTileset->getRootTile()->getContent().getExternalContent(); + if (!pExternalContent) { + return; + } + + const auto* pVoxelExtension = + pExternalContent + ->getExtension(); + if (!pVoxelExtension) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Tileset %s does not contain voxel content, so CesiumVoxelMetadataComponent will have no effect."), + *pOwner->GetName()); + return; + } + + 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 VoxelMetadataClassification : public MaterialNodeClassification { + UMaterialExpressionCustom* RaymarchNode = nullptr; + UMaterialExpressionMaterialFunctionCall* BreakFloat4Node = nullptr; +}; + +struct MaterialResourceLibrary { + FString ShaderTemplate; + UMaterialFunctionMaterialLayer* MaterialLayerTemplate; + TObjectPtr pDefaultVolumeTexture; + + MaterialResourceLibrary() { + 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 !ShaderTemplate.IsEmpty() && MaterialLayerTemplate && + pDefaultVolumeTexture; + } +}; + +/** + * 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}"; + +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 VoxelMetadataClassification +ClassifyNodes(UMaterialFunctionMaterialLayer* Layer) { + VoxelMetadataClassification Classification; + for (const TObjectPtr& pNode : + Layer->GetExpressionCollection().Expressions) { + // Check if this node is marked as autogenerated. + if (pNode->Desc.StartsWith( + AutogeneratedMessage, + ESearchCase::Type::CaseSensitive)) { + 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; +} + +static void ClearAutoGeneratedNodes( + UMaterialFunctionMaterialLayer* 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) { + 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; + } + + 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() == RaymarchDescription) { + 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 TArrayView& ExpressionInputs = + NewExpression->GetInputsView(); + 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; + } + } + } + } + + // Save this to offset some nodes later. + int32 SetMaterialAttributesOffset = + BreakFloat4Node->MaterialExpressionEditorX; + NodeX = DataSectionX; + NodeY = DataSectionY; + + // Inspired by HLSLMaterialTranslator.cpp. Similar to MaterialTemplate.ush, + // CesiumVoxelTemplate.usf contains "%s" formatters that will be replaced + // with generated code. + FLazyPrintf LazyPrintf(*ResourceLibrary.ShaderTemplate); + 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 = + Cast(ResourceLibrary.pDefaultVolumeTexture); + 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 += SetMaterialAttributesOffset + 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); + SetMaterialAttributes->MaterialExpressionEditorX = NodeX; + SetMaterialAttributes->MaterialExpressionEditorY = NodeY; + MaterialState.OneTimeGeneratedNodes.Add(SetMaterialAttributes); + } + + 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); + } + } + } + + 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); + 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); + + 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() { + ACesium3DTileset* pTileset = Cast(this->GetOwner()); + if (!pTileset) { + return; + } + + MaterialResourceLibrary ResourceLibrary; + ResourceLibrary.pDefaultVolumeTexture = this->pDefaultVolumeTexture; + 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.ConnectionInputRemap, + MaterialState.ConnectionOutputRemap); + 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/CesiumVoxelRendererComponent.cpp b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp new file mode 100644 index 000000000..2a82091fe --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.cpp @@ -0,0 +1,713 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "CesiumVoxelRendererComponent.h" +#include "CalcBounds.h" +#include "Cesium3DTileset.h" +#include "CesiumGltfComponent.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_CesiumVoxel.MI_CesiumVoxel")), + 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; + + Super::BeginDestroy(); +} + +bool UCesiumVoxelRendererComponent::IsReadyForFinishDestroy() { + if (this->_pOctree.IsValid() && !this->_pOctree->canBeDestroyed()) { + return false; + } + + if (this->_pDataTextures.IsValid()) { + this->_pDataTextures->pollLoadingSlots(); + return this->_pDataTextures->canBeDestroyed(); + } + + return Super::IsReadyForFinishDestroy(); +} + +namespace { +EVoxelGridShape getVoxelGridShape( + const Cesium3DTilesSelection::BoundingVolume& boundingVolume) { + if (std::get_if(&boundingVolume)) { + return EVoxelGridShape::Box; + } + + return EVoxelGridShape::Invalid; +} + +void setVoxelBoxProperties( + UCesiumVoxelRendererComponent* pVoxelComponent, + 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)); + + // 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"), + 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(); +} + +} // 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->_pOctree->getTexture()); + 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->_pDataTextures->getTexture(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); + } + } + } + + const glm::uvec3& tileCount = + pVoxelComponent->_pDataTextures->getTileCountAlongAxes(); + pVoxelMaterial->SetVectorParameterValueByInfo( + FMaterialParameterInfo( + UTF8_TO_TCHAR("Tile Count"), + EMaterialParameterAssociation::LayerParameter, + 0), + FVector(tileCount.x, tileCount.y, tileCount.z)); + } + + 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 does not contain the metadata class that is referenced by its voxel content."), + *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 has invalid voxel grid 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 a root bounding volume that is not supported for voxels."), + *pTilesetActor->GetName()) + return nullptr; + } + + const Cesium3DTiles::Class* pVoxelClass = + &tilesetMetadata.schema->classes.at(voxelClassId); + CESIUM_ASSERT(pVoxelClass != nullptr); + + UCesiumVoxelRendererComponent* pVoxelComponent = + NewObject(pTilesetActor); + pVoxelComponent->SetMobility(pTilesetActor->GetRootComponent()->Mobility); + pVoxelComponent->SetFlags( + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + + 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 the transformation between y-up (glTF) to z-up (3D Tiles). + dataDimensions = + glm::uvec3(dataDimensions.x, dataDimensions.z, dataDimensions.y); + } + + 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, + dataDimensions, + pVoxelMesh->GetScene()->GetFeatureLevel(), + knownTileCount); + } + + uint32 maximumTileCount = + pVoxelComponent->_pDataTextures + ? pVoxelComponent->_pDataTextures->getMaximumTileCount() + : 1; + pVoxelComponent->_pOctree = MakeUnique(maximumTileCount); + pVoxelComponent->_loadedNodeIds.reserve(maximumTileCount); + + 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; +} + +namespace { +template +void forEachRenderableVoxelTile(const auto& tiles, Func&& f) { + for (size_t i = 0; i < tiles.size(); i++) { + const Cesium3DTilesSelection::Tile::ConstPointer& 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* pGltf = static_cast( + pRenderContent->getRenderResources()); + 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 + // own renderable content. In both cases, the tile is ignored here. + continue; + } + + const TArray& Children = pGltf->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) { + 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); + }); + + size_t existingNodeCount = this->_loadedNodeIds.size(); + size_t destroyedNodeCount = 0; + size_t addedNodeCount = 0; + + if (this->_pDataTextures) { + // 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. + this->_needsOctreeUpdate |= + 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); + this->_needsOctreeUpdate |= createdNewNode || addedToDataTexture; + + if (!addedToDataTexture) { + continue; + } else if (addNodeIndex < this->_loadedNodeIds.size()) { + this->_loadedNodeIds[addNodeIndex] = currentTileId; + } else { + 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()) { + 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. + this->_needsOctreeUpdate |= 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 (this->_needsOctreeUpdate) { + this->_needsOctreeUpdate = !this->_pOctree->updateTexture(); + } +} + +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(); + } +} + +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 new file mode 100644 index 000000000..34d044bb8 --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumVoxelRendererComponent.h @@ -0,0 +1,129 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#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 "VoxelMegatextures.h" +#include "VoxelOctree.h" + +#include +#include +#include +#include + +#include "CesiumVoxelRendererComponent.generated.h" + +namespace Cesium3DTilesSelection { +class TilesetMetadata; +} // namespace Cesium3DTilesSelection + +class ACesium3DTileset; +struct FCesiumVoxelClassDescription; + +UCLASS() +/** + * A component that enables raymarched voxel rendering across an entire tileset. + * + * Unlike triangle meshes, voxels are rendered by raymarching in an Unreal + * material assigned 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; + bool IsReadyForFinishDestroy() 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; + + void UpdateTransformFromCesium(const glm::dmat4& CesiumToUnrealTransform); + + /** + * 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); + +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); + + static double computePriority(double sse); + + struct VoxelTileUpdateInfo { + const UCesiumGltfVoxelComponent* pComponent; + double sse; + double priority; + }; + + struct PriorityLessComparator { + bool + operator()(const VoxelTileUpdateInfo& lhs, const VoxelTileUpdateInfo& rhs) { + return lhs.priority < rhs.priority; + } + }; + + using MaxPriorityQueue = std::priority_queue< + VoxelTileUpdateInfo, + std::vector, + PriorityLessComparator>; + + TUniquePtr _pOctree; + TUniquePtr _pDataTextures; + std::vector _loadedNodeIds; + MaxPriorityQueue _visibleTileQueue; + bool _needsOctreeUpdate; +}; diff --git a/Source/CesiumRuntime/Private/CreateGltfOptions.h b/Source/CesiumRuntime/Private/CreateGltfOptions.h index ce8c7a5ee..2aab2db1d 100644 --- a/Source/CesiumRuntime/Private/CreateGltfOptions.h +++ b/Source/CesiumRuntime/Private/CreateGltfOptions.h @@ -9,13 +9,17 @@ #include "CesiumGltf/Model.h" #include "CesiumGltf/Node.h" #include "LoadGltfResult.h" +#include "VoxelGridShape.h" +#include #include /** * 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 +56,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 +76,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 +104,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/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/Private/LoadGltfResult.h b/Source/CesiumRuntime/Private/LoadGltfResult.h index 8a6dddc45..6e1452cc3 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" @@ -157,6 +159,11 @@ struct LoadedPrimitiveResult { CesiumGltf::IndexAccessorType IndexAccessor; #pragma endregion + + /** + * The index of the property attribute that is used by voxels. + */ + std::optional voxelPropertyAttributeIndex; }; /** 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/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 +}; diff --git a/Source/CesiumRuntime/Private/VoxelMegatextures.cpp b/Source/CesiumRuntime/Private/VoxelMegatextures.cpp new file mode 100644 index 000000000..5a0d758ed --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelMegatextures.cpp @@ -0,0 +1,386 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "VoxelMegatextures.h" + +#include "CesiumGltfVoxelComponent.h" +#include "CesiumLifetime.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 "UObject/Package.h" + +#include +#include +#include + +using namespace CesiumGltf; +using namespace EncodedFeaturesMetadata; + +FVoxelMegatextures::FVoxelMegatextures( + const FCesiumVoxelClassDescription& description, + const glm::uvec3& slotDimensions, + ERHIFeatureLevel::Type featureLevel, + uint32 knownTileCount) + : _slots(), + _loadingSlots(), + _pEmptySlotsHead(nullptr), + _pOccupiedSlotsHead(nullptr), + _slotDimensions(slotDimensions), + _tileCountAlongAxes(0), + _maximumTileCount(0), + _propertyMap() { + 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. + 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; + for (const FCesiumPropertyAttributePropertyDescription& Property : + description.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, texelSizeBytes, 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 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 + // 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( + LogCesium, + Error, + TEXT( + "Unable to create data textures for voxel dataset due to limited memory.")) + return; + } + + glm::uvec3 textureDimensions = this->_tileCountAlongAxes * slotDimensions; + + 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->pPrevious = i > 0 ? &_slots[i - 1] : nullptr; + pSlot->pNext = i < _slots.size() - 1 ? &_slots[i + 1] : nullptr; + } + + this->_pEmptySlotsHead = &this->_slots[0]; + + // Create the actual textures. + for (auto& propertyIt : this->_propertyMap) { + FTextureResource* pResource = FCesiumTextureResource::CreateEmpty( + TextureGroup::TEXTUREGROUP_8BitData, + textureDimensions.x, + textureDimensions.y, + textureDimensions.z, + propertyIt.Value.encodedFormat.format, + TextureFilter::TF_Nearest, + TextureAddress::TA_Clamp, + TextureAddress::TA_Clamp, + false) + .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(pResource); + propertyIt.Value.pTexture = pTexture; + + ENQUEUE_RENDER_COMMAND(Cesium_InitResource) + ([pTexture, pResource = pTexture->GetResource()]( + FRHICommandListImmediate& RHICmdList) { + if (!pResource) + return; + + pResource->SetTextureReference( + pTexture->TextureReference.TextureReferenceRHI); + pResource->InitResource(FRHICommandListImmediate::Get()); + }); + } +} + +FVoxelMegatextures::~FVoxelMegatextures() { + CESIUM_ASSERT(this->canBeDestroyed()); +} + +bool FVoxelMegatextures::canBeDestroyed() const { + return this->_loadingSlots.size() == 0; +} + +UTexture* FVoxelMegatextures::getTexture(const FString& attributeId) const { + const TextureData* pProperty = this->_propertyMap.Find(attributeId); + return pProperty ? pProperty->pTexture : nullptr; +} + +/** + * 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 FVoxelMegatextures::directCopyToTexture( + const FCesiumPropertyAttributeProperty& property, + const FVoxelMegatextures::TextureData& data, + const FUpdateTextureRegion3D& updateRegion) { + if (!data.pTexture) + return; + + const uint8* pData = + reinterpret_cast(property.getAccessorData()); + + ENQUEUE_RENDER_COMMAND(Cesium_DirectCopyVoxels) + ([pTexture = data.pTexture, + format = data.encodedFormat.format, + &property, + updateRegion, + texelSizeBytes = data.texelSizeBytes, + pData](FRHICommandListImmediate& RHICmdList) { + 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; + uint32 srcDepthPitch = + updateRegion.Width * updateRegion.Height * texelSizeBytes; + + RHIUpdateTexture3D( + pResource->TextureRHI, + 0, + updateRegion, + srcRowPitch, + srcDepthPitch, + pData); + }); +} + +/*static*/ void FVoxelMegatextures::incrementalWriteToTexture( + const FCesiumPropertyAttributeProperty& property, + const FVoxelMegatextures::TextureData& data, + const FUpdateTextureRegion3D& updateRegion) { + if (!data.pTexture) + return; + + ENQUEUE_RENDER_COMMAND(Cesium_IncrementalWriteVoxels) + ([pTexture = data.pTexture, + format = data.encodedFormat.format, + &property, + updateRegion, + texelSizeBytes = + data.texelSizeBytes](FRHICommandListImmediate& RHICmdList) { + FTextureResource* pResource = + IsValid(pTexture) ? pTexture->GetResource() : nullptr; + if (!pResource) + return; + + FUpdateTexture3DData UpdateData = + RHIBeginUpdateTexture3D(pResource->TextureRHI, 0, updateRegion); + + for (uint32 z = 0; z < updateRegion.Depth; z++) { + for (uint32 y = 0; y < updateRegion.Height; y++) { + int64_t sourceIndex = int64_t( + 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 FVoxelMegatextures::add(const UCesiumGltfVoxelComponent& voxelComponent) { + int64 slotIndex = this->reserveNextSlot(); + if (slotIndex < 0) { + return -1; + } + + // Compute the update region for the data textures. + FUpdateTextureRegion3D updateRegion; + updateRegion.Width = this->_slotDimensions.x; + updateRegion.Height = this->_slotDimensions.y; + updateRegion.Depth = this->_slotDimensions.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->_slotDimensions.z; + updateRegion.DestY = indexY * this->_slotDimensions.y; + updateRegion.DestX = indexX * this->_slotDimensions.x; + + uint32 index = static_cast(slotIndex); + + for (const auto& PropertyIt : this->_propertyMap) { + const FCesiumPropertyAttributeProperty& property = + UCesiumPropertyAttributeBlueprintLibrary::FindProperty( + voxelComponent.PropertyAttribute, + PropertyIt.Key); + + if (UCesiumPropertyAttributePropertyBlueprintLibrary:: + GetPropertyAttributePropertyStatus(property) != + ECesiumPropertyAttributePropertyStatus::Valid) { + continue; + } + + if (property.getAccessorStride() == PropertyIt.Value.texelSizeBytes) { + directCopyToTexture(property, PropertyIt.Value, updateRegion); + } else { + incrementalWriteToTexture(property, PropertyIt.Value, updateRegion); + } + } + + this->_slots[slotIndex].fence.emplace().BeginFence(); + this->_loadingSlots.insert(slotIndex); + + return slotIndex; +} + +bool FVoxelMegatextures::release(int64_t slotIndex) { + if (slotIndex < 0 || slotIndex >= int64(this->_slots.size())) { + return false; // Index out of bounds + } + + Slot* pSlot = &this->_slots[slotIndex]; + pSlot->fence.reset(); + + if (pSlot->pPrevious) { + pSlot->pPrevious->pNext = pSlot->pNext; + } + if (pSlot->pNext) { + pSlot->pNext->pPrevious = pSlot->pPrevious; + } + + // Move to list of empty slots (as the new head) + pSlot->pNext = this->_pEmptySlotsHead; + if (pSlot->pNext) { + pSlot->pNext->pPrevious = pSlot; + } + + pSlot->pPrevious = nullptr; + this->_pEmptySlotsHead = pSlot; + + return true; +} + +int64 FVoxelMegatextures::reserveNextSlot() { + // Remove head from list of empty slots + FVoxelMegatextures::Slot* pSlot = this->_pEmptySlotsHead; + if (!pSlot) { + return -1; + } + + this->_pEmptySlotsHead = pSlot->pNext; + + if (this->_pEmptySlotsHead) { + this->_pEmptySlotsHead->pPrevious = nullptr; + } + + // 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 pSlot->index; +} + +bool FVoxelMegatextures::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 FVoxelMegatextures::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/VoxelMegatextures.h b/Source/CesiumRuntime/Private/VoxelMegatextures.h new file mode 100644 index 000000000..dfcf0ab80 --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelMegatextures.h @@ -0,0 +1,186 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumCommon.h" +#include "CesiumMetadataValueType.h" +#include "EncodedFeaturesMetadata.h" +#include "RenderCommandFence.h" + +#include +#include +#include + +struct FCesiumVoxelClassDescription; +class FCesiumTextureResource; +class UCesiumGltfVoxelComponent; + +/** + * 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 + * 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 FVoxelMegatextures { +public: + /** + * @brief Constructs a set of voxel data textures. + * + * @param description The voxel class description, indicating which metadata + * attributes to encode. + * @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 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. + */ + FVoxelMegatextures( + const FCesiumVoxelClassDescription& description, + const glm::uvec3& slotDimensions, + ERHIFeatureLevel::Type featureLevel, + uint32 knownTileCount); + + ~FVoxelMegatextures(); + + /** + * @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 { + return static_cast(this->_slots.size()); + } + + /** + * @brief Gets the number of tiles along each dimension of the textures. + */ + glm::uvec3 getTileCountAlongAxes() const { return this->_tileCountAlongAxes; } + + /** + * @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; + + /** + * @brief Whether or not all slots in the textures are occupied. + */ + bool isFull() const { return this->_pEmptySlotsHead == nullptr; } + + /** + * @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 add(const UCesiumGltfVoxelComponent& voxelComponent); + + /** + * @brief Releases the slot at the specified index, making the space available + * for another voxel tile. + */ + bool release(int64_t slotIndex); + + /** + * @brief Whether or not the slot at the given index has loaded data. + */ + 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: + /** + * Value constants taken from CesiumJS. + */ + static const uint32 MaximumTextureMemoryBytes = 512 * 1024 * 1024; + static const uint32 DefaultTextureMemoryBytes = 128 * 1024 * 1024; + + /** + * @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* pNext = nullptr; + Slot* pPrevious = nullptr; + std::optional fence; + }; + + struct TextureData { + /** + * @brief The texture format used to store encoded property values. + */ + EncodedFeaturesMetadata::EncodedPixelFormat encodedFormat; + + /** + * @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; + }; + + /** + * @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 reserveNextSlot(); + + std::vector _slots; + std::unordered_set _loadingSlots; + + Slot* _pEmptySlotsHead; + Slot* _pOccupiedSlotsHead; + + glm::uvec3 _slotDimensions; + 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..9f2e71091 --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelOctree.cpp @@ -0,0 +1,396 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "VoxelOctree.h" +#include "CesiumLifetime.h" +#include "CesiumRuntime.h" +#include "CesiumTextureResource.h" + +#include +#include + +using namespace CesiumGeometry; +using namespace Cesium3DTilesContent; + +/*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, + 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, + result); /* parentTextureIndex */ + + // Pad the data as necessary for the texture copy. + uint32 regionWidth = this->_tilesPerRow * TexelsPerNode * sizeof(uint32); + uint32 regionHeight = glm::ceil((float)result.size() / regionWidth); + uint32 expectedSize = regionWidth * regionHeight; + + if (result.size() != expectedSize) { + result.resize(expectedSize, std::byte(0)); + } + + // Compute the area of the texture that actually needs updating. + uint32 texelCount = result.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->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->GetResource(), &result, region, sourcePitch]( + FRHICommandListImmediate& RHICmdList) { + RHIUpdateTexture2D( + pResource->TextureRHI, + 0, + region, + sourcePitch, + reinterpret_cast(result.data())); + }); +} + +void UVoxelOctreeTexture::insertNodeData( + std::vector& data, + uint32 textureIndex, + ENodeFlag nodeFlag, + uint16 dataValue, + uint8 renderableLevelDifference) { + uint32_t dataIndex = textureIndex * 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. + 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( + const FVoxelOctree& octree, + const CesiumGeometry::OctreeTileID& tileId, + uint32& nodeCount, + uint32 octreeIndex, + uint32 textureIndex, + uint32 parentOctreeIndex, + 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( + result, + parentTextureIndex, + ENodeFlag::Internal, + octreeIndex); + insertNodeData( + result, + 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( + octree, + childId, + nodeCount, + octreeIndex, + textureIndex, + parentOctreeIndex, + parentTextureIndex + childIndex++, + result); + } + } 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(result, parentTextureIndex, flag, value, levelDifference); + nodeCount++; + } +} + +size_t FVoxelOctree::OctreeTileIDHash::operator()( + const CesiumGeometry::OctreeTileID& tileId) const { + // 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) + // 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(uint32 maximumTileCount) + : _nodes(), _pTexture(nullptr), _fence(std::nullopt), _data() { + CesiumGeometry::OctreeTileID rootTileID(0, 0, 0, 0); + this->_nodes.insert({rootTileID, Node()}); + this->_pTexture = UVoxelOctreeTexture::create(maximumTileCount); +} + +FVoxelOctree::~FVoxelOctree() { + CESIUM_ASSERT(!this->_fence || this->_fence->IsFenceComplete()); +} + +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) { + return this->_nodes.contains(TileID) ? &this->_nodes.at(TileID) : nullptr; +} + +bool FVoxelOctree::createNode(const CesiumGeometry::OctreeTileID& TileID) { + FVoxelOctree::Node* pNode = this->getNode(TileID); + if (pNode) { + return false; + } + + // Create the target node first. + this->_nodes.insert({TileID, Node()}); + pNode = &this->_nodes[TileID]; + + // Starting from the target node, traverse the tree upwards and create the + // 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--) { + OctreeTileID parentTileID = + *ImplicitTilingUtilities::getParentID(currentTileID); + if (this->_nodes.contains(parentTileID)) { + foundExistingParent = true; + } else { + this->_nodes.insert({parentTileID, Node()}); + } + + FVoxelOctree::Node* pParent = &this->_nodes[parentTileID]; + pNode->pParent = pParent; + + // The parent *shouldn't* have children at this point. Otherwise, our + // target node would have already been found. + for (const CesiumGeometry::OctreeTileID& child : + ImplicitTilingUtilities::getChildren(parentTileID)) { + if (!this->_nodes.contains(child)) { + this->_nodes.insert({child, Node()}); + this->_nodes[child].pParent = 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. + OctreeTileID parentTileId = *ImplicitTilingUtilities::getParentID(tileId); + OctreeChildren siblings = ImplicitTilingUtilities::getChildren(parentTileId); + for (const OctreeTileID& siblingId : siblings) { + 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 OctreeTileID& siblingId : siblings) { + 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 { + const FVoxelOctree::Node* pNode = this->getNode(TileID); + if (!pNode) { + return false; + } + + return pNode->dataIndex > 0 || pNode->hasChildren; +} + +bool FVoxelOctree::updateTexture() { + if (!this->_pTexture || (this->_fence && !this->_fence->IsFenceComplete())) { + return false; + } + + this->_fence.reset(); + this->_pTexture->update(*this, this->_data); + + // Prevent changes to the data while the texture is updating on the render + // thread. + this->_fence.emplace().BeginFence(); + return true; +} + +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 new file mode 100644 index 000000000..9ce6acd52 --- /dev/null +++ b/Source/CesiumRuntime/Private/VoxelOctree.h @@ -0,0 +1,290 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "Engine/Texture2D.h" +#include "RenderCommandFence.h" + +#include +#include +#include + +class FVoxelOctree; + +/** + * A texture that encodes information from \ref FVoxelOctree. + */ +class UVoxelOctreeTexture : public UTexture2D { +public: + /** + * @brief Creates a new texture with the specified tile capacity. + */ + static UVoxelOctreeTexture* create(uint32 maximumTileCount); + + /** + * @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, std::vector& result); + +private: + /** + * @brief The number of texels used to represent a node in the texture. + * + * 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; + + /** + * @brief The maximum allowed width for the texture. Value taken from + * CesiumJS. + */ + static const uint32 MaximumOctreeTextureWidth = 2048; + + /** + * @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 given data vector, automatically + * expanding it if the target index is out-of-bounds. + */ + void insertNodeData( + std::vector& data, + uint32 textureIndex, + ENodeFlag nodeFlag, + uint16 dataValue, + 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 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. + */ + void encodeNode( + const FVoxelOctree& octree, + const CesiumGeometry::OctreeTileID& tileId, + uint32& nodeCount, + uint32 octreeIndex, + uint32 textureIndex, + uint32 parentOctreeIndex, + uint32 parentTextureIndex, + std::vector& result); + + uint32 _tilesPerRow; +}; + +/** + * @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 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 FVoxelDataTextures. + * + * The connection with \ref FVoxelDataTextures is managed externally by + * \UCesiumVoxelRendererComponent. + */ +class FVoxelOctree { +public: + /** + * @brief A tile in an implicitly tiled octree. + */ + struct Node { + /** + * @brief Points to the parent of the node, if it exists. + */ + Node* pParent = nullptr; + /** + * @brief Whether the tile's children exist in the octree. + */ + bool hasChildren = false; + /** + * @brief The tile's last known screen space error. + */ + double lastKnownScreenSpaceError = 0.0; + /** + * @brief The index of the slot that this tile occupies in \ref + * 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; + }; + + /** + * @brief Constructs an initially empty octree with the specified tile + * capacity. + */ + FVoxelOctree(uint32 maximumTileCount); + + ~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. + */ + const Node* getNode(const CesiumGeometry::OctreeTileID& TileID) const; + + /** + * @copydoc getNode + */ + Node* getNode(const CesiumGeometry::OctreeTileID& TileID); + + /** + * @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); + + /** + * @brief Retrieves the texture containing the encoded octree. + */ + UTexture2D* getTexture() const { return this->_pTexture; } + + bool updateTexture(); + + bool canBeDestroyed() const; + +private: + 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. + */ + using NodeMap = + std::unordered_map; + NodeMap _nodes; + + UVoxelOctreeTexture* _pTexture; + uint32_t _tilesPerRow; + 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 _data; +}; diff --git a/Source/CesiumRuntime/Public/Cesium3DTileset.h b/Source/CesiumRuntime/Public/Cesium3DTileset.h index 2aa951bb1..1dce711eb 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,14 @@ class CESIUMRUNTIME_API ACesium3DTileset : public AActor { UCesiumEllipsoid* OldEllipsoid, UCesiumEllipsoid* NewEllpisoid); + /** + * Creates and attaches a \ref UCesiumVoxelRendererComponent for rendering + * voxel data. + */ + void + createVoxelRenderer(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 +1341,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/CesiumFeaturesMetadataDescription.h b/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h index f0fe787e2..a077962ec 100644 --- a/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h +++ b/Source/CesiumRuntime/Public/CesiumFeaturesMetadataDescription.h @@ -216,6 +216,60 @@ 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. + */ + 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/CesiumPropertyAttributeProperty.h b/Source/CesiumRuntime/Public/CesiumPropertyAttributeProperty.h index 7274be520..514d599e8 100644 --- a/Source/CesiumRuntime/Public/CesiumPropertyAttributeProperty.h +++ b/Source/CesiumRuntime/Public/CesiumPropertyAttributeProperty.h @@ -123,6 +123,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/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h new file mode 100644 index 000000000..d62984127 --- /dev/null +++ b/Source/CesiumRuntime/Public/CesiumVoxelMetadataComponent.h @@ -0,0 +1,157 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumFeaturesMetadataDescription.h" +#include "Templates/UniquePtr.h" + +#if WITH_EDITOR +#include "Materials/MaterialFunctionMaterialLayer.h" +#endif + +#include "CesiumVoxelMetadataComponent.generated.h" + +/** + * @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; + + /** + * 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: + TObjectPtr pDefaultVolumeTexture; + +#if WITH_EDITOR + static const FString ShaderPreviewTemplate; + void UpdateShaderPreview(); +#endif +}; diff --git a/extern/cesium-native b/extern/cesium-native index 3e0874263..d1ab84535 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 3e08742630050710c11da6c5b0c875c0af6fe4d2 +Subproject commit d1ab84535290739e5a0b5b3463495128a6954a77