To incorporate debug rendering in Prodigy Engine, I designed a Debug Render System capable of handling its own cameras as well as being able to draw both 2D and 3D shapes in the scene.
Here’s what the scene looks like when drawing all debug shapes:
To incorporate this, I implemented a DebugRenderer class which serves as the interface used by a game to call into. Below is the header for my DebugRender system:
//------------------------------------------------------------------------------------------------------------------------------ // System to render debugging data for specified objects in the system. //------------------------------------------------------------------------------------------------------------------------------ enum eDebugRenderSpace { DEBUG_RENDER_SCREEN, // renders in screen space (ie, backbuffer), defined when initializing the system DEBUG_RENDER_WORLD, // is rendered in the world; }; enum eDebugRenderMode { DEBUG_RENDER_USE_DEPTH, DEBUG_RENDER_ALWAYS, // always draw, does not write to depth DEBUG_RENDER_XRAY, // render behind (greater, no write), then in front (lequal, write) // darken or thin the line during behind render to differentiate from it being in front; }; //------------------------------------------------------------------------------------------------------------------------------ struct DebugRenderOptionsT { eDebugRenderSpace space = DEBUG_RENDER_SCREEN; eDebugRenderMode mode = DEBUG_RENDER_USE_DEPTH; Rgba beginColor = Rgba::WHITE; Rgba endColor = Rgba::RED; // 2D common - these are where I will treat the "origin" // of the screen for drawing - defaulting to bottom-left corner; // used for SCREEN and CAMERA modes, ignored for WORLD Vec4 positionRatioAndOffset = Vec4( 0.0f, 0.0f, 0.0f, 0.0f ); // this is bottom-left corner // 3D (WORLD space) common (ignored for SCREEN & CAMERA) Matrix44 modelTransform = Matrix44::IDENTITY; // local transform to do to the object; bool relativeCoordinates = false; ObjectProperties* objectProperties = nullptr; }; //------------------------------------------------------------------------------------------------------------------------------ class DebugRender { public: DebugRender(); ~DebugRender(); //Events for Event system and dev console static bool DisableDebugRender( EventArgs& args ); static bool EnableDebugRender( EventArgs& args ); static bool ClearAllLiveObjects( EventArgs& args ); static bool ClearAllLiveScreenObjects( EventArgs& args ); static bool ClearAllLiveWorldObjects( EventArgs& args ); void Startup(RenderContext* renderContext); void SetClientDimensions(int height, int width); void SetDebugFont( BitmapFont* font); void Shutdown(); void BeginFrame(); void EndFrame(); void Update(float deltaTime); void CleanUpObjects(); void DebugRenderToScreen() const; //This renders to the screen camera void DebugRenderToCamera() const; //This renders in the world camera void Setup2DCamera() const; void Setup3DCamera(Camera* const camera); void SetObjectMatrixForPosition( Vec3 position ) const; void SetObjectMatrixForBillBoard( Vec3 position ) const; void SetWorldSize2D(const Vec2& worldMin, const Vec2& worldMax); Vec2 GetRelativePosInWorld2D(const Vec2& positionInWorld); Camera& Get2DCamera(); //------------------------------------------------------------------------------------------------------------------------------ // 2D Debug Rendering (defaults to SCREEN) //------------------------------------------------------------------------------------------------------------------------------ void DebugRenderPoint2D( DebugRenderOptionsT options, const Vec2& position, float duration = 0.f, float size = DEFAULT_POINT_SIZE ); void DebugRenderLine2D( DebugRenderOptionsT options, const Vec2& start, const Vec2& end, float duration = 0.f, float lineWidth = DEFAULT_LINE_WIDTH ); void DebugRenderQuad2D( DebugRenderOptionsT options, AABB2 const &quad, float duration = 0.f, TextureView *view = nullptr); void DebugRenderWireQuad2D( DebugRenderOptionsT options, AABB2 const &quad, float duration = 0.f, float thickness = DEFAULT_WIRE_WIDTH_2D ); void DebugRenderDisc2D( DebugRenderOptionsT options, Disc2D const &disc, float duration = 0.f); void DebugRenderRing2D( DebugRenderOptionsT options, Disc2D const &disc, float duration = 0.f, float thickness = DEFAULT_DISC_THICKNESS ); void DebugRenderText2D( DebugRenderOptionsT options, const Vec2& startPosition, const Vec2& endPosition, char const *format, float fontHeight = DEFAULT_TEXT_HEIGHT, float duration = 0.f, ... ); void DebugRenderArrow2D( DebugRenderOptionsT options, const Vec2& start, const Vec2& end, float duration = 0.f, float lineWidth = DEFAULT_LINE_WIDTH ); // Helpers void DebugRenderPoint2D( const Vec2& position, float duration = 0.f, float size = DEFAULT_POINT_SIZE ); void DebugRenderLine2D( const Vec2& start, const Vec2& end, float duration = 0.f, float lineWidth = DEFAULT_LINE_WIDTH ); void DebugRenderQuad2D( AABB2 const &quad, float duration = 0.f, TextureView *view = nullptr); void DebugRenderWireQuad2D( AABB2 const &quad, float duration = 0.f, float thickness = DEFAULT_WIRE_WIDTH_2D ); void DebugRenderDisc2D( Disc2D const &disc, float duration = 0.f); void DebugRenderRing2D( Disc2D const &disc, float duration = 0.f, float thickness = DEFAULT_DISC_THICKNESS ); void DebugRenderText2D( const Vec2& startPosition, const Vec2& endPosition, char const *format, float fontHeight = DEFAULT_TEXT_HEIGHT, float duration = 0.f, ... ); void DebugRenderArrow2D( const Vec2& start, const Vec2& end, float duration = 0.f, float lineWidth = DEFAULT_LINE_WIDTH ); //------------------------------------------------------------------------------------------------------------------------------ // Text Logs //------------------------------------------------------------------------------------------------------------------------------ void DebugAddToLog( DebugRenderOptionsT options, char const* format, const Rgba& color = Rgba::ORANGE, float duration = 0.f, ...); void DebugAddToLog( char const* format, const Rgba& color = Rgba::ORANGE, float duration = 0.f, ...); //------------------------------------------------------------------------------------------------------------------------------ // 3D Rendering (will always default to WORLD) //------------------------------------------------------------------------------------------------------------------------------ void DebugRenderPoint( DebugRenderOptionsT options, const Vec3& position, float duration = 0.f, float size = DEFAULT_POINT_SIZE_3D, TextureView* texture = nullptr ); void DebugRenderLine( DebugRenderOptionsT options, const Vec3& start, const Vec3& end, float duration = 0.f, float lineWidth = DEFAULT_LINE_WIDTH_3D ); void DebugRenderSphere( DebugRenderOptionsT options, Vec3 center, float radius, float duration = 0.f, TextureView* texture = nullptr ); void DebugRenderBox( DebugRenderOptionsT options, const AABB3& box, const Vec3& position, float duration = 0.f, TextureView* texture = nullptr ); void DebugRenderQuad( DebugRenderOptionsT options, const AABB2& quad, const Vec3& position, float duration = 0.f, TextureView* texture = nullptr, bool billBoarded = true); void DebugRenderCapsule(DebugRenderOptionsT options, const Capsule3D& capsule, const Vec3& position, float duration = 0.f, TextureView* texture = nullptr); // Helpers void DebugRenderPoint( const Vec3& position, float duration = 0.f, float size = DEFAULT_POINT_SIZE_3D, TextureView* texture = nullptr ); void DebugRenderLine(const Vec3& start, const Vec3& end, float duration = 0.f); void DebugRenderSphere( Vec3 center, float radius, float duration = 0.f, TextureView* texture = nullptr ); void DebugRenderBox( const AABB3& box, const Vec3& position, float duration = 0.f, TextureView* texture = nullptr ); void DebugRenderQuad( const AABB2& quad, const Vec3& position, float duration = 0.f, TextureView* texture = nullptr, bool billBoarded = true); void DebugRenderCapsule( const Capsule3D& capsule, const Vec3& position, float duration = 0.f, TextureView* texture = nullptr); //Render Wire shapes void DebugRenderWireSphere( DebugRenderOptionsT options, Vec3 center, float radius, float duration = 0.f, TextureView* texture = nullptr ); void DebugRenderWireBox( DebugRenderOptionsT options, const AABB3& box, const Vec3& position, float duration = 0.f, TextureView* texture = nullptr ); void DebugRenderWireCapsule(DebugRenderOptionsT options, const Capsule3D& capsule, const Vec3& position, float duration = 0.f, TextureView* texture = nullptr); void DebugRenderWireCapsule(const Capsule3D& capsule, const Vec3& position, float duration = 0.f, TextureView* texture = nullptr); void DebugRenderArrow( DebugRenderOptionsT options, Vec3 start, Vec3 end, float base_thickness, float head_thickness ); void DebugRenderBasis( DebugRenderOptionsT options, Matrix44 const &basis, float lineWidth = DEFAULT_LINE_WIDTH ); //------------------------------------------------------------------------------------------------------------------------------ // Rendering 3D Text //------------------------------------------------------------------------------------------------------------------------------ void DebugRenderText3D( DebugRenderOptionsT options, const Vec3& position, const Vec2& pivot, char const *format, float fontHeight = DEFAULT_TEXT_HEIGHT_3D, float duration = 0.f, bool isBillboarded = true, ... ); private: void DebugRenderToLog() const; //This renders all the debug logs to the screen //Draw methods 2D void DrawPoint2D ( const DebugRenderOptionsT* renderObject ) const; void DrawLine2D ( const DebugRenderOptionsT* renderObject ) const; void DrawQuad2D ( const DebugRenderOptionsT* renderObject ) const; void DrawWireQuad2D ( const DebugRenderOptionsT* renderObject ) const; void DrawDisc2D ( const DebugRenderOptionsT* renderObject ) const; void DrawRing2D ( const DebugRenderOptionsT* renderObject ) const; void DrawArrow2D ( const DebugRenderOptionsT* renderObject ) const; void DrawText2D ( const DebugRenderOptionsT* renderObject ) const; //Draw methods 3D void DrawPoint3D ( const DebugRenderOptionsT* renderObject ) const; void DrawQuad3D ( const DebugRenderOptionsT* renderObject ) const; void DrawLine3D ( const DebugRenderOptionsT* renderObject ) const; void DrawSphere ( const DebugRenderOptionsT* renderObject ) const; void DrawWireSphere ( const DebugRenderOptionsT* renderObject ) const; void DrawBox ( const DebugRenderOptionsT* renderObject ) const; void DrawWireBox ( const DebugRenderOptionsT* renderObject ) const; void DrawText3D ( const DebugRenderOptionsT* renderObject ) const; void DrawCapsule3D (const DebugRenderOptionsT* renderObject) const; void DrawWireCapsule3D (const DebugRenderOptionsT* renderObject) const; //Destroy objects functions void DestroyAllScreenObjects(); void DestroyAllWorldObjects(); private: const static Rgba DEBUG_INFO; const static Rgba DEBUG_ECHO; const static Rgba DEBUG_BG_COLOR; const float m_logFontHeight = DEFAULT_LOG_TEXT_HEIGHT; RenderContext* m_renderContext = nullptr; int m_clientWidth = 0; int m_clientHeight = 0; Camera* m_debug3DCam = nullptr; Camera* m_debug2DCam = nullptr; bool m_canRender = true; //2D world size Vec2 m_worldMin2D = Vec2::ZERO; Vec2 m_worldMax2D = Vec2::ZERO; BitmapFont* m_debugFont = nullptr; Shader* m_xrayShader = nullptr; Shader* m_defaultShader = nullptr; std::string m_defaultShaderPath = "default_unlit.xml"; std::string m_xmlShaderPath = "default_unlit_xray.xml"; //Keep a reference to the DebugRender instance for use with event systems static DebugRender* s_debugRender; //Store all debug objects with their render options and other data std::vector<DebugRenderOptionsT> m_worldRenderObjects; std::vector<DebugRenderOptionsT> m_screenRenderObjects; std::vector<DebugRenderOptionsT> m_printLogObjects; };
This also needed some classes to define the debug object properties for the different shapes I intend to draw. To do so, I incorporated a DebugObjectProperties file which contained an ObjectProperties base class and all shapes inherited their properties from it.
The code for the DebugObjectProperties is highlighted below:
//------------------------------------------------------------------------------------------------------------------------------ enum eDebugRenderObject { DEBUG_RENDER_POINT, DEBUG_RENDER_POINT3D, DEBUG_RENDER_LINE, DEBUG_RENDER_LINE3D, DEBUG_RENDER_QUAD, DEBUG_RENDER_WIRE_QUAD, DEBUG_RENDER_QUAD3D, DEBUG_RENDER_DISC, DEBUG_RENDER_RING, DEBUG_RENDER_SPHERE, DEBUG_RENDER_WIRE_SPHERE, DEBUG_RENDER_BOX, DEBUG_RENDER_WIRE_BOX, DEBUG_RENDER_ARROW, DEBUG_RENDER_ARROW3D, DEBUG_RENDER_CAPSULE, DEBUG_RENDER_WIRE_CAPSULE, DEBUG_RENDER_BASIS, DEBUG_RENDER_TEXT, DEBUG_RENDER_TEXT3D, DEBUG_RENDER_LOG }; //------------------------------------------------------------------------------------------------------------------------------ class ObjectProperties { public: virtual ~ObjectProperties(); eDebugRenderObject m_renderObjectType; float m_durationSeconds = 0.0f; // show for a single frame float m_startDuration = 0.f; Rgba m_currentColor = Rgba::WHITE; CPUMesh* m_mesh; };
//------------------------------------------------------------------------------------------------------------------------------ // 2D Render Objects //------------------------------------------------------------------------------------------------------------------------------ // Point //------------------------------------------------------------------------------------------------------------------------------ class Point2DProperties : public ObjectProperties { public: explicit Point2DProperties(eDebugRenderObject renderObject, const Vec2& screenPosition, float durationSeconds = 0.f, float size = DEFAULT_POINT_SIZE); virtual ~Point2DProperties(); public: Vec2 m_screenPosition = Vec2::ZERO; float m_size = DEFAULT_POINT_SIZE; }; // Line //------------------------------------------------------------------------------------------------------------------------------ class Line2DProperties : public ObjectProperties { public: explicit Line2DProperties(eDebugRenderObject renderObject, const Vec2& startPos, const Vec2& endPos, float durationSeconds = 0.f, float lineWidth = DEFAULT_LINE_WIDTH); virtual ~Line2DProperties(); public: Vec2 m_startPos = Vec2::ZERO; Vec2 m_endPos = Vec2::ZERO; float m_lineWidth = DEFAULT_LINE_WIDTH; }; // Arrow //------------------------------------------------------------------------------------------------------------------------------ class Arrow2DProperties : public ObjectProperties { public: explicit Arrow2DProperties(eDebugRenderObject renderObject, const Vec2& start, const Vec2& end, float durationSeconds = 0.f, float lineWidth = DEFAULT_LINE_WIDTH); virtual ~Arrow2DProperties(); public: Vec2 m_startPos = Vec2::ZERO; Vec2 m_endPos = Vec2::ZERO; float m_lineWidth = DEFAULT_LINE_WIDTH; Vec2 m_lineEnd = Vec2::ZERO; Vec2 m_arrowTip = Vec2::ZERO; Vec2 m_lineNorm = Vec2::ZERO; }; // Quad //------------------------------------------------------------------------------------------------------------------------------ class Quad2DProperties : public ObjectProperties { public: explicit Quad2DProperties( eDebugRenderObject renderObject, const AABB2& quad, float durationSeconds = 0.f, float thickness = DEFAULT_WIRE_WIDTH_2D, TextureView* texture = nullptr ); virtual ~Quad2DProperties(); public: TextureView* m_texture = nullptr; float m_thickness = DEFAULT_WIRE_WIDTH_2D; //Only used when rendering as a wire box AABB2 m_quad; }; // Disc //------------------------------------------------------------------------------------------------------------------------------ class Disc2DProperties : public ObjectProperties { public: explicit Disc2DProperties( eDebugRenderObject renderObject, const Disc2D& disc, float thickness, float durationSeconds = 0.f); virtual ~Disc2DProperties(); public: Disc2D m_disc; float m_thickness; //Only used when rendering it as a ring };
// Point //------------------------------------------------------------------------------------------------------------------------------ class Point3DProperties : public ObjectProperties { public: explicit Point3DProperties( eDebugRenderObject renderObject, const Vec3& position, float size = DEFAULT_POINT_SIZE_3D, float durationSeconds = 0.f, TextureView* texture = nullptr); virtual ~Point3DProperties(); public: Vec3 m_position = Vec3::ZERO; TextureView* m_texture = nullptr; float m_size = DEFAULT_POINT_SIZE_3D; AABB2 m_point; }; // Line //------------------------------------------------------------------------------------------------------------------------------ class Line3DProperties : public ObjectProperties { public: explicit Line3DProperties( eDebugRenderObject renderObject, const Vec3& startPos, const Vec3& endPos, float durationSeconds = 0.f, float lineWidth = DEFAULT_LINE_WIDTH); virtual ~Line3DProperties(); public: Vec3 m_startPos = Vec3::ZERO; Vec3 m_endPos = Vec3::ZERO; Vec3 m_center = Vec3::ZERO; float m_lineWidth = DEFAULT_LINE_WIDTH; AABB2 m_line; }; // Quad //------------------------------------------------------------------------------------------------------------------------------ class Quad3DProperties : public ObjectProperties { public: explicit Quad3DProperties( eDebugRenderObject renderObject, const AABB2& quad, const Vec3& position, float durationSeconds = 0.f, TextureView* texture = nullptr, bool billBoarded = true ); virtual ~Quad3DProperties(); public: Vec3 m_position = Vec3::ZERO; TextureView* m_texture = nullptr; bool m_billBoarded = true; AABB2 m_quad; }; // Sphere //------------------------------------------------------------------------------------------------------------------------------ class SphereProperties : public ObjectProperties { public: explicit SphereProperties( eDebugRenderObject renderObject, const Vec3& center, float radius, float durationSeconds = 0.f, TextureView* texture = nullptr); virtual ~SphereProperties(); public: Vec3 m_center = Vec3::ZERO; float m_radius = 0.f; TextureView* m_texture = nullptr; }; // Capsule //------------------------------------------------------------------------------------------------------------------------------ class CapsuleProperties : public ObjectProperties { public: explicit CapsuleProperties( eDebugRenderObject renderObject, const Capsule3D& capsule, const Vec3& position, float durationSeconds = 0.f, TextureView* texture = nullptr); virtual ~CapsuleProperties(); public: Vec3 m_position = Vec3::ZERO; TextureView* m_texture = nullptr; Capsule3D m_capsule; }; // Box //------------------------------------------------------------------------------------------------------------------------------ class BoxProperties : public ObjectProperties { public: explicit BoxProperties( eDebugRenderObject renderObject, const AABB3& box, const Vec3& position, float durationSeconds = 0.f, TextureView* texture = nullptr); virtual ~BoxProperties(); public: Vec3 m_position = Vec3::ZERO; TextureView* m_texture = nullptr; AABB3 m_box; };
//------------------------------------------------------------------------------------------------------------------------------ // Text Render Objects //------------------------------------------------------------------------------------------------------------------------------ class TextProperties : public ObjectProperties { public: explicit TextProperties( eDebugRenderObject renderObject, const Vec3& position, const Vec2& pivot, const std::string& text, float fontHeight, float durationSeconds = 0.f, bool isBillboarded = true); explicit TextProperties( eDebugRenderObject renderObject, const Vec2& startPosition, const Vec2& endPosition, const std::string& text, float fontHeight, float durationSeconds = 0.f); virtual ~TextProperties(); public: //For 3D Vec3 m_position = Vec3::ZERO; Vec2 m_pivot = Vec2::ZERO; bool m_isBillboarded = true; //For 2D Vec2 m_startPosition = Vec2::ZERO; Vec2 m_endPosition = Vec2::ZERO; float m_fontHeight = DEFAULT_TEXT_HEIGHT_3D; std::string m_string; }; //------------------------------------------------------------------------------------------------------------------------------ // Text Log Entry //------------------------------------------------------------------------------------------------------------------------------ class LogProperties : public ObjectProperties { public: explicit LogProperties(eDebugRenderObject renderObject, const Rgba& printColor, const std::string& printString, float durationSeconds = 0.f); virtual ~LogProperties(); public: Rgba m_printColor = Rgba::WHITE; std::string m_string; };
With these property classes, the Debug Renderer is able to create the required shapes and populate their required properties. The only thing left to do now is to create the required objects to draw and queue them for a draw call.
Below is the logic for the DebugRenderWireSphere function
void DebugRender::DebugRenderWireSphere( DebugRenderOptionsT options, Vec3 center, float radius, float duration /*= 0.f*/, TextureView* texture /*= nullptr */ ) { options.objectProperties = new SphereProperties(DEBUG_RENDER_WIRE_SPHERE, center, radius, duration, texture); m_worldRenderObjects.push_back(options); }
As can be seen, the function populates debug render options with a new objectProperty which in this case is a WireSphere
When the debug render function is called, the DebugRenderToCamera function executes all draw calls for 3D objects in the scene. This is what the DebugRenderToCamera function on DebugRenderer looks like:
void DebugRender::DebugRenderToCamera() const { if(!m_canRender) { return; } //Use this method to render to the world camera int vectorSize = static_cast<int>(m_worldRenderObjects.size()); for(int objectIndex = 0; objectIndex < vectorSize; objectIndex++) { const DebugRenderOptionsT* renderObject = &m_worldRenderObjects[objectIndex]; switch(renderObject->objectProperties->m_renderObjectType) { case DEBUG_RENDER_POINT3D: { DrawPoint3D(renderObject); } break; case DEBUG_RENDER_LINE3D: { DrawLine3D(renderObject); } break; case DEBUG_RENDER_SPHERE: { DrawSphere(renderObject); } break; case DEBUG_RENDER_BOX: { DrawBox(renderObject); } break; case DEBUG_RENDER_QUAD3D: { DrawQuad3D(renderObject); } break; case DEBUG_RENDER_WIRE_SPHERE: { DrawWireSphere(renderObject); } break; case DEBUG_RENDER_WIRE_BOX: { DrawWireBox(renderObject); } break; case DEBUG_RENDER_TEXT3D: { DrawText3D(renderObject); } break; case DEBUG_RENDER_CAPSULE: { DrawCapsule3D(renderObject); } break; case DEBUG_RENDER_WIRE_CAPSULE: { DrawWireCapsule3D(renderObject); } break; default: { ERROR_AND_DIE("The debug object is not yet defined in DebugRenderToCamera"); } break; } } }
Taking a look at the code for DrawWireSphere, the implementation followed is highlighted below:
void DebugRender::DrawWireSphere( const DebugRenderOptionsT* renderObject ) const { SphereProperties* objectProperties = reinterpret_cast<SphereProperties*>(renderObject->objectProperties); if(objectProperties->m_renderObjectType != DEBUG_RENDER_WIRE_SPHERE) { ERROR_AND_DIE("Object recieved in DebugRender was not a Sphere. Check inputs"); } //Setup mesh here GPUMesh sphere = GPUMesh( m_renderContext ); sphere.CreateFromCPUMesh<Vertex_PCU>( objectProperties->m_mesh, GPU_MEMORY_USAGE_STATIC ); SetObjectMatrixForPosition(objectProperties->m_center); //Setup the textures on the render context m_renderContext->BindTextureViewWithSampler(0U, objectProperties->m_texture); m_renderContext->SetRasterStateWireFrame(); switch (renderObject->mode) { case DEBUG_RENDER_USE_DEPTH: m_renderContext->SetDepth(true); m_renderContext->DrawMesh(&sphere); break; case DEBUG_RENDER_ALWAYS: m_renderContext->SetDepth(false); m_renderContext->DrawMesh(&sphere); break; case DEBUG_RENDER_XRAY: { //Make 2 draw calls here //One with compare op lequals and one with compare op greater than (edit alpha on that one) m_renderContext->SetGlobalTint(Rgba::DARK_GREY); m_xrayShader->SetDepth(eCompareOp::COMPARE_GREATER, false); m_renderContext->BindShader(m_xrayShader); m_renderContext->DrawMesh(&sphere); m_renderContext->BindShader(m_defaultShader); m_renderContext->DrawMesh(&sphere); } break; } m_renderContext->CreateAndSetDefaultRasterState(); }
Similar to the case of the WireSphere, all other debug shapes follow a similar queue and draw paradigm.
Here’s a view with only the 3D objects being rendered:
Considerations Made
- This was a debug system so performance wasn’t as important
- I want something easy to use
- Should be able to support all primitive shapes
- Should be able to use it’s own debug cameras to render
- 2D and 3D should be completely different draw calls and draw logic
Retrospectives
What went well 🙂
- Implemented the system to be simple for end-user
- Support for all primitive shapes in 2D and 3D
- Support for text in 2D and 3D
What went wrong 🙁
- Still pretty over-engineered in my opinion
- Couldn’t get arrows in 3D
- No ico-sphere support
What I would do differently 🙂
- Simplify the system to not have to create multiple objects
- Re-use shapes by keeping some static CPU Meshes
- Implement 3D arrows and a basis vector