New skyboxrenderer + moved meshrenderer inside engine and out from main to clean that up

This commit is contained in:
2026-02-22 17:16:21 -05:00
parent a781facd48
commit 932c45d844
14 changed files with 399 additions and 7 deletions

View File

@@ -231,3 +231,62 @@ graph LR
1. **Cubemap source****Procedural gradient skybox** chosen because asset loading infrastructure is not yet established.
2. **Sampler heap** — Evaluate what exists. However, with a procedural skybox, we won't need a sampler for Phase 3! (The sky color can be procedurally generated from the reconstructed view direction).
3. **Specular****Blinn-Phong** specular, structured in an agnostic way so PBR (physically based rendering) parameters can be plugged in later.
---
## Phase 4: Forward Rendered Entity Lights
A simple, fast forward renderer using a Global Structured Buffer for entity-based point lights.
### Approach: Bindless Global Lights Buffer
Instead of passing each light via PushConstants, we pass a `StructuredBuffer<PointLight>` index and iterate over the active lights in the fragment shader.
### New C++ Components
```cpp
struct PointLight
{
Vector3 Position;
float Radius;
Vector3 Color;
float Intensity;
};
```
1. **Lights Buffer Allocation**: Allocate a `StructuredBuffer` large enough for all potential lights (e.g., max 1024).
2. **Buffer Upload**: Loop through the active game entity lights every frame and upload them using the TransferBuffer system.
3. **PushData Update**:
- `uint32 LightsBufferIndex`
- `uint32 ActiveLightCount`
### Shader Design
**Fragment Shader Expansion** loop over all `ActiveLightCount` to compute the attenuation and diffuse, then accumulate.
```hlsl
StructuredBuffer<PointLight> lights = ResourceDescriptorHeap[LightsBufferIndex];
float3 totalDiffuse = Color.rgb * LightColor * NdotL; // Include Directional Sun
for (uint i = 0; i < ActiveLightCount; ++i)
{
PointLight light = lights[i];
float3 lightVector = light.Position - WorldPosition;
float distance = length(lightVector);
if (distance > light.Radius) continue;
float3 L = lightVector / distance;
float NdotL = saturate(dot(N, L));
float attenuation = 1.0 - saturate(distance / light.Radius);
attenuation *= attenuation; // inverse square-ish
totalDiffuse += Color.rgb * light.Color * light.Intensity * NdotL * attenuation;
}
```
> [!NOTE]
> Passing `WorldPosition` to the Fragment shader from the Vertex shader is required for positional lighting.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -13,8 +13,8 @@ Output main(uint vertexIndex : SV_VertexID)
// Retrieve the vertex buffer using SM6.6 bindless syntax
ByteAddressBuffer buffer = ResourceDescriptorHeap[BufferIndex];
// TextureIndex is used as vertex offset for consolidated buffer (depth-tested at 0, overlay at halfMax)
uint actualVertexIndex = vertexIndex + TextureIndex;
// VertexOffset is used as vertex offset for consolidated buffer (depth-tested at 0, overlay at halfMax)
uint actualVertexIndex = vertexIndex + VertexOffset;
// Vertex layout: float3 Position (12 bytes) + float4 Color (16 bytes) = 28 bytes stride
uint stride = 28;

View File

@@ -0,0 +1,26 @@
#include "RootConstants.hlsl"
float4 main(float3 ViewDir : TEXCOORD0) : SV_Target0
{
float3 dir = normalize(ViewDir);
// Simple Procedural Gradient Skybox Colors
float3 skyZenithColor = float3(0.05f, 0.15f, 0.45f); // Deep blue top
float3 skyHorizonColor = float3(0.4f, 0.6f, 0.9f); // Light blue horizon
float3 groundColor = float3(0.1f, 0.1f, 0.15f); // Dark ground
float t = dir.z; // -1 to 1 based on vertical alignment
float3 finalColor = groundColor;
if (t > 0.0f) {
// Blend from horizon to zenith
finalColor = lerp(skyHorizonColor, skyZenithColor, t);
} else {
// Ground - quick blend from horizon to ground
finalColor = lerp(skyHorizonColor, groundColor, saturate(-t * 2.0f));
}
// Combine with overall ambient lighting scale
return float4(finalColor, 1.0f);
}

View File

@@ -0,0 +1,74 @@
#include "RootConstants.hlsl"
struct Output
{
float3 ViewDir : TEXCOORD0;
float4 Position : SV_Position;
};
float4x4 Inverse(float4x4 m)
{
float n11 = m[0][0], n12 = m[1][0], n13 = m[2][0], n14 = m[3][0];
float n21 = m[0][1], n22 = m[1][1], n23 = m[2][1], n24 = m[3][1];
float n31 = m[0][2], n32 = m[1][2], n33 = m[2][2], n34 = m[3][2];
float n41 = m[0][3], n42 = m[1][3], n43 = m[2][3], n44 = m[3][3];
float t11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44;
float t12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44;
float t13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44;
float t14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34;
float det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14;
float idet = 1.0f / det;
float4x4 ret;
ret[0][0] = t11 * idet;
ret[0][1] = (n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44) * idet;
ret[0][2] = (n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44) * idet;
ret[0][3] = (n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43) * idet;
ret[1][0] = t12 * idet;
ret[1][1] = (n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44) * idet;
ret[1][2] = (n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44) * idet;
ret[1][3] = (n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43) * idet;
ret[2][0] = t13 * idet;
ret[2][1] = (n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44) * idet;
ret[2][2] = (n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44) * idet;
ret[2][3] = (n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43) * idet;
ret[3][0] = t14 * idet;
ret[3][1] = (n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34) * idet;
ret[3][2] = (n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34) * idet;
ret[3][3] = (n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33) * idet;
return ret;
}
Output main(uint vertexID : SV_VertexID)
{
Output output;
// Fullscreen triangle properties
// vertexID 0 -> uv(0,0) -> pos(-1, 1)
// vertexID 1 -> uv(2,0) -> pos( 3, 1)
// vertexID 2 -> uv(0,2) -> pos(-1, -3)
float2 uv = float2((vertexID << 1) & 2, vertexID & 2);
// Map uv to clip space position.
// Z is set to 1.0 (far plane in reverse-Z or standard depth)
// Assuming standard Z projection ranges from 0 to 1, we use 1.0 for the far plane.
// Depending on reverse-z, this might need to be 0.0, but let's stick to standard 1.0
// depth for skyboxes.
float4 clipPos = float4(uv * 2.0 - 1.0, 1.0, 1.0);
clipPos.y = -clipPos.y; // Flip Y for D3D
output.Position = clipPos;
// Reconstruct view direction from clip space
float4x4 inverseVP = Inverse(ViewProjection);
float4 worldPos = mul(inverseVP, clipPos);
output.ViewDir = worldPos.xyz / worldPos.w;
return output;
}

View File

@@ -133,4 +133,46 @@ namespace Juliet
result.m[3][3] = 0.0f;
return result;
}
[[nodiscard]] inline Matrix MatrixInverse(const Matrix& m)
{
Matrix out = {};
float m00 = m.m[0][0], m01 = m.m[0][1], m02 = m.m[0][2], m03 = m.m[0][3];
float m10 = m.m[1][0], m11 = m.m[1][1], m12 = m.m[1][2], m13 = m.m[1][3];
float m20 = m.m[2][0], m21 = m.m[2][1], m22 = m.m[2][2], m23 = m.m[2][3];
float m30 = m.m[3][0], m31 = m.m[3][1], m32 = m.m[3][2], m33 = m.m[3][3];
out.m[0][0] = m11 * m22 * m33 - m11 * m23 * m32 - m21 * m12 * m33 + m21 * m13 * m32 + m31 * m12 * m23 - m31 * m13 * m22;
out.m[1][0] = -m10 * m22 * m33 + m10 * m23 * m32 + m20 * m12 * m33 - m20 * m13 * m32 - m30 * m12 * m23 + m30 * m13 * m22;
out.m[2][0] = m10 * m21 * m33 - m10 * m23 * m31 - m20 * m11 * m33 + m20 * m13 * m31 + m30 * m11 * m23 - m30 * m13 * m21;
out.m[3][0] = -m10 * m21 * m32 + m10 * m22 * m31 + m20 * m11 * m32 - m20 * m12 * m31 - m30 * m11 * m22 + m30 * m12 * m21;
out.m[0][1] = -m01 * m22 * m33 + m01 * m23 * m32 + m21 * m02 * m33 - m21 * m03 * m32 - m31 * m02 * m23 + m31 * m03 * m22;
out.m[1][1] = m00 * m22 * m33 - m00 * m23 * m32 - m20 * m02 * m33 + m20 * m03 * m32 + m30 * m02 * m23 - m30 * m03 * m22;
out.m[2][1] = -m00 * m21 * m33 + m00 * m23 * m31 + m20 * m01 * m33 - m20 * m03 * m31 - m30 * m01 * m23 + m30 * m03 * m21;
out.m[3][1] = m00 * m21 * m32 - m00 * m22 * m31 - m20 * m01 * m32 + m20 * m02 * m31 + m30 * m01 * m22 - m30 * m02 * m21;
out.m[0][2] = m01 * m12 * m33 - m01 * m13 * m32 - m11 * m02 * m33 + m11 * m03 * m32 + m31 * m02 * m13 - m31 * m03 * m12;
out.m[1][2] = -m00 * m12 * m33 + m00 * m13 * m32 + m10 * m02 * m33 - m10 * m03 * m32 - m30 * m02 * m13 + m30 * m03 * m12;
out.m[2][2] = m00 * m11 * m33 - m00 * m13 * m31 - m10 * m01 * m33 + m10 * m03 * m31 + m30 * m01 * m13 - m30 * m03 * m11;
out.m[3][2] = -m00 * m11 * m32 + m00 * m12 * m31 + m10 * m01 * m32 - m10 * m02 * m31 - m30 * m01 * m12 + m30 * m02 * m11;
out.m[0][3] = -m01 * m12 * m23 + m01 * m13 * m22 + m11 * m02 * m23 - m11 * m03 * m22 - m21 * m02 * m13 + m21 * m03 * m12;
out.m[1][3] = m00 * m12 * m23 - m00 * m13 * m22 - m10 * m02 * m23 + m10 * m03 * m22 + m20 * m02 * m13 - m20 * m03 * m12;
out.m[2][3] = -m00 * m11 * m23 + m00 * m13 * m21 + m10 * m01 * m23 - m10 * m03 * m21 - m20 * m01 * m13 + m20 * m03 * m11;
out.m[3][3] = m00 * m11 * m22 - m00 * m12 * m21 - m10 * m01 * m22 + m10 * m02 * m21 + m20 * m01 * m12 - m20 * m02 * m11;
float det = m00 * out.m[0][0] + m01 * out.m[1][0] + m02 * out.m[2][0] + m03 * out.m[3][0];
if (det != 0.0f)
{
float invDet = 1.0f / det;
for (int r = 0; r < 4; ++r)
for (int c = 0; c < 4; ++c)
out.m[r][c] *= invDet;
}
return out;
}
} // namespace Juliet

View File

@@ -8,13 +8,14 @@ namespace Juliet
struct Engine
{
IApplication* Application = nullptr;
IApplication* Application = nullptr;
Arena* PlatformArena = nullptr;
};
void InitializeEngine(JulietInit_Flags flags);
void ShutdownEngine();
void LoadApplication(IApplication& app);
void LoadApplication(IApplication& app, Arena* arena);
void UnloadApplication();
void RunEngine();

View File

@@ -0,0 +1,29 @@
#pragma once
#include <Core/Math/Matrix.h>
#include <Juliet.h>
namespace Juliet
{
struct RenderPass;
struct CommandList;
struct Window;
struct GraphicsPipeline;
struct GraphicsDevice;
struct SkyboxRenderer
{
GraphicsDevice* Device;
GraphicsPipeline* Pipeline;
};
[[nodiscard]] JULIET_API bool InitializeSkyboxRenderer(NonNullPtr<GraphicsDevice> device,
NonNullPtr<Window> window);
JULIET_API void ShutdownSkyboxRenderer();
JULIET_API void RenderSkybox(NonNullPtr<RenderPass> pass, NonNullPtr<CommandList> cmdList, const Matrix& viewProjection);
#if ALLOW_SHADER_HOT_RELOAD
JULIET_API void ReloadSkyboxShaders();
#endif
} // namespace Juliet

View File

@@ -7,6 +7,8 @@
#include <Graphics/DebugDisplay.h>
#include <Graphics/Graphics.h>
#include <Graphics/RenderPass.h>
#include <Graphics/MeshRenderer.h>
#include <Graphics/SkyboxRenderer.h>
#include <time.h>
#ifdef JULIET_ENABLE_IMGUI
@@ -37,6 +39,11 @@ namespace Juliet
if (device)
{
DebugDisplay_Initialize(device);
if (Window* window = EngineInstance.Application->GetPlatformWindow())
{
InitializeMeshRenderer(EngineInstance.PlatformArena, device, window);
InitializeSkyboxRenderer(device, window);
}
}
#ifdef JULIET_ENABLE_IMGUI
@@ -73,6 +80,8 @@ namespace Juliet
if (device)
{
DebugDisplay_Shutdown(device);
ShutdownSkyboxRenderer();
ShutdownMeshRenderer();
}
}
@@ -121,6 +130,9 @@ namespace Juliet
// Debug display flush (inside render pass)
Camera debugCamera = EngineInstance.Application->GetDebugCamera();
DebugDisplay_Flush(cmdList, pass, debugCamera);
// Note: The MeshRenderer and SkyboxRenderer draw calls are still inside Application->OnRender
// They shouldn't be moved here directly without an interface since they require PushData.
#ifdef JULIET_ENABLE_IMGUI
// ImGui rendering (always last before EndRenderPass)
@@ -155,9 +167,10 @@ namespace Juliet
ShutdownLogManager();
}
void LoadApplication(IApplication& app)
void LoadApplication(IApplication& app, Arena* platformArena)
{
EngineInstance.Application = &app;
EngineInstance.Application = &app;
EngineInstance.PlatformArena = platformArena;
EngineInstance.Application->Init();
// Systems depending on Window/GraphicsDevice

View File

@@ -0,0 +1,102 @@
#include <Graphics/SkyboxRenderer.h>
#include <Core/HAL/Filesystem/Filesystem.h>
#include <Core/Logging/LogManager.h>
#include <Core/Logging/LogTypes.h>
#include <Graphics/GraphicsDevice.h>
#include <Graphics/PushConstants.h>
namespace Juliet
{
namespace
{
SkyboxRenderer g_SkyboxRenderer;
} // namespace
bool InitializeSkyboxRenderer(NonNullPtr<GraphicsDevice> device, NonNullPtr<Window> window)
{
bool result = true;
GraphicsDevice* graphicsDevice = g_SkyboxRenderer.Device = device.Get();
String skyboxVSEntry = WrapString("main");
ShaderCreateInfo skyboxVSCI = {};
skyboxVSCI.EntryPoint = skyboxVSEntry;
skyboxVSCI.Stage = ShaderStage::Vertex;
String vsPath = GetAssetPath(WrapString("Skybox.vert.dxil"));
Shader* skyboxVS = CreateShader(graphicsDevice, vsPath, skyboxVSCI);
String skyboxFSEntry = WrapString("main");
ShaderCreateInfo skyboxFSCI = {};
skyboxFSCI.EntryPoint = skyboxFSEntry;
skyboxFSCI.Stage = ShaderStage::Fragment;
String fsPath = GetAssetPath(WrapString("Skybox.frag.dxil"));
Shader* skyboxFS = CreateShader(graphicsDevice, fsPath, skyboxFSCI);
ColorTargetDescription colorTargetDesc = {};
colorTargetDesc.Format = GetSwapChainTextureFormat(graphicsDevice, window);
GraphicsPipelineCreateInfo skyboxPipelineCI = {};
skyboxPipelineCI.VertexShader = skyboxVS;
skyboxPipelineCI.FragmentShader = skyboxFS;
skyboxPipelineCI.PrimitiveType = PrimitiveType::TriangleList;
skyboxPipelineCI.TargetInfo.ColorTargetDescriptions = &colorTargetDesc;
skyboxPipelineCI.TargetInfo.NumColorTargets = 1;
skyboxPipelineCI.TargetInfo.DepthStencilFormat = TextureFormat::D32_FLOAT;
skyboxPipelineCI.TargetInfo.HasDepthStencilTarget = true;
skyboxPipelineCI.RasterizerState.FillMode = FillMode::Solid;
skyboxPipelineCI.RasterizerState.CullMode = CullMode::None;
skyboxPipelineCI.RasterizerState.FrontFace = FrontFace::Clockwise;
skyboxPipelineCI.DepthStencilState.EnableDepthTest = true;
skyboxPipelineCI.DepthStencilState.EnableDepthWrite = false;
skyboxPipelineCI.DepthStencilState.CompareOperation = CompareOperation::LessOrEqual;
g_SkyboxRenderer.Pipeline = CreateGraphicsPipeline(graphicsDevice, skyboxPipelineCI);
if (g_SkyboxRenderer.Pipeline == nullptr)
{
LogError(LogCategory::Graphics, "Failed to create skybox pipeline!");
result = false;
}
if (skyboxVS) DestroyShader(graphicsDevice, skyboxVS);
if (skyboxFS) DestroyShader(graphicsDevice, skyboxFS);
return result;
}
void ShutdownSkyboxRenderer()
{
if (g_SkyboxRenderer.Pipeline)
{
DestroyGraphicsPipeline(g_SkyboxRenderer.Device, g_SkyboxRenderer.Pipeline);
g_SkyboxRenderer.Pipeline = nullptr;
}
g_SkyboxRenderer = {};
}
void RenderSkybox(NonNullPtr<RenderPass> pass, NonNullPtr<CommandList> cmdList, const Matrix& viewProjection)
{
if (!g_SkyboxRenderer.Pipeline)
{
return;
}
PushData pushData = {};
pushData.ViewProjection = viewProjection;
pushData.Model = MatrixIdentity();
BindGraphicsPipeline(pass, g_SkyboxRenderer.Pipeline);
SetPushConstants(cmdList, ShaderStage::Vertex, 0, sizeof(pushData) / 4, &pushData);
SetPushConstants(cmdList, ShaderStage::Fragment, 0, sizeof(pushData) / 4, &pushData);
DrawPrimitives(pass, 3, 1, 0, 0);
}
#if ALLOW_SHADER_HOT_RELOAD
void ReloadSkyboxShaders()
{
// TODO
}
#endif
} // namespace Juliet

View File

@@ -24,7 +24,7 @@
#include <Graphics/MeshRenderer.h>
#include <Graphics/RenderPass.h>
#include <Graphics/VertexData.h>
#include <Juliet.h>
#include <Graphics/SkyboxRenderer.h>
#ifdef JULIET_ENABLE_IMGUI
#include <imgui.h>
@@ -135,6 +135,50 @@ void JulietApplication::Init()
{
return;
}
// Create Skybox Pipeline
String skyboxVSEntry = WrapString("main");
ShaderCreateInfo skyboxVSCI = {};
skyboxVSCI.EntryPoint = skyboxVSEntry;
skyboxVSCI.Stage = ShaderStage::Vertex;
String vsPath = GetAssetPath(WrapString("Skybox.vert.dxil"));
Shader* skyboxVS = CreateShader(GraphicsDevice, vsPath, skyboxVSCI);
String skyboxFSEntry = WrapString("main");
ShaderCreateInfo skyboxFSCI = {};
skyboxFSCI.EntryPoint = skyboxFSEntry;
skyboxFSCI.Stage = ShaderStage::Fragment;
String fsPath = GetAssetPath(WrapString("Skybox.frag.dxil"));
Shader* skyboxFS = CreateShader(GraphicsDevice, fsPath, skyboxFSCI);
ColorTargetDescription colorTargetDesc = {};
colorTargetDesc.Format = GetSwapChainTextureFormat(GraphicsDevice, MainWindow);
GraphicsPipelineCreateInfo skyboxPipelineCI = {};
skyboxPipelineCI.VertexShader = skyboxVS;
skyboxPipelineCI.FragmentShader = skyboxFS;
skyboxPipelineCI.PrimitiveType = PrimitiveType::TriangleList;
skyboxPipelineCI.TargetInfo.ColorTargetDescriptions = &colorTargetDesc;
skyboxPipelineCI.TargetInfo.NumColorTargets = 1;
skyboxPipelineCI.TargetInfo.DepthStencilFormat = TextureFormat::D32_FLOAT;
skyboxPipelineCI.TargetInfo.HasDepthStencilTarget = true;
skyboxPipelineCI.RasterizerState.FillMode = FillMode::Solid;
skyboxPipelineCI.RasterizerState.CullMode = CullMode::None;
skyboxPipelineCI.RasterizerState.FrontFace = FrontFace::Clockwise;
skyboxPipelineCI.DepthStencilState.EnableDepthTest = true;
skyboxPipelineCI.DepthStencilState.EnableDepthWrite = false;
skyboxPipelineCI.DepthStencilState.CompareOperation = CompareOperation::LessOrEqual;
SkyboxPipeline = CreateGraphicsPipeline(GraphicsDevice, skyboxPipelineCI);
if (SkyboxPipeline == nullptr)
{
LogError(LogCategory::Graphics, "Failed to create skybox pipeline!");
Running = false;
}
if (skyboxVS) DestroyShader(GraphicsDevice, skyboxVS);
if (skyboxFS) DestroyShader(GraphicsDevice, skyboxFS);
}
GameCode.Functions = reinterpret_cast<void**>(&Game);
@@ -324,6 +368,7 @@ void JulietApplication::OnRender(RenderPass* pass, CommandList* cmd)
pushData.LightColor = { 1.0f, 0.95f, 0.8f };
pushData.AmbientIntensity = 0.2f;
RenderSkybox(pass, cmd, pushData.ViewProjection);
RenderMeshes(pass, cmd, pushData);
}

View File

@@ -43,6 +43,7 @@ class JulietApplication : public Juliet::IApplication
Juliet::HotReloadCode GameCode = {};
Juliet::GraphicsPipeline* GraphicsPipeline = {};
Juliet::Texture* DepthBuffer = {};
Juliet::GraphicsPipeline* SkyboxPipeline = {};
Juliet::Arena* GameArena = nullptr;
Juliet::Arena* GameScratchArena = nullptr;