SmartShape2D icon indicating copy to clipboard operation
SmartShape2D copied to clipboard

Refactor rendering

Open mphe opened this issue 6 months ago • 9 comments

Background

The edge generation step ends with the _meshes property of the shape being filled with SS2D_Mesh[1] objects, which in turn consist of one or more ArrayMesh objects. In _draw(), the _meshes array will be flattened [2][3] into another Array[SS2D_Mesh] where every SS2D_Mesh contains only one ArrayMesh. For every SS2D_Mesh in this list, an SS2D_Shape_Render[4] node will be instantiated. It receives the corresponding mesh and sets respective render properties (material, z-index, ...) on itself and renders the mesh in its own _draw(). I'm unsure how large these ArrayMeshes are, but I'm pretty sure they directly correspond to the SS2D_IndexMaps created during generation, i.e. the edges generated as part of one index map will be part of the same ArrayMesh.

Performance

In a simple shape with 4 vertices and only a single edge material, this process takes around 0.1 ms, which might not sound like much but is actually insanely long for this use-case. However, this time does not change with the vertex count, e.g. with hundreds of points, the processing time would still be around 0.1 ms. This is because the costly operations here are not the rendering, but the flattening (which includes resource duplication) and node instantiation. With a single edge material, there will only be a single rendering node with a single mesh. With that in mind, 0.1 ms runtime cost is even more insane. But if we add corner materials, tapers, etc., the complexity rises.

Considering the sharp_corner_tapering example, there are suddenly 21 rendering nodes for the left shape and 10 for the right (including 1 for the fill texture each). The computation time also increased to 0.5 ms for the left shape and 0.25 ms for the right shape. As a one-time computation during runtime I'd call this negligible, but in editor when continously editing points and regenerating the shape, this is actually pretty expensive.

Issues

This approach has obviously several downsides:

  • High computation cost
    • Lots of data juggling and resource duplication
    • Deleting and instantiating lots of rendering nodes (but they are mostly cached at least)
  • Lots of nodes for the engine to handle
  • Lots of draw calls (might get batched by the renderer, though)
  • Shapes need to regenerate meshes at runtime (unlike collisions)
  • Shapes are not clickable in editor (which is IMO one of the major annoyances with this plugin)

New Approach Proposal

IIRC, we already briefly talked about this approach on Discord. The idea is to generate a single ArrayMesh, which contains all the meshes and assign it to a single MeshInstance2D for rendering. ArrayMesh supports multiple surfaces, so different meshes with different materials can be accomodated in the same ArrayMesh, hence only one rendering node is needed. In order to prevent the number of surfaces from exploding with increasing shape complexity and to maintain the correct z-order, a custom sort function has to be employed which sorts meshes by z-index and material. Sub-meshes with the same material can be merged in the same surface, otherwise a new surface is used. Instead of flattening the mesh array and creating a bunch of rendering nodes, we sort it and directly generate a single ArrayMesh from it. It might even be possible to further improve this step between edge generation and rendering but this requires further investigation.

Besides improving performance, this approach also has the following advantages:

  • Shapes are now clickable, because MeshInstance2D is clickable
  • Basically out-of-box baking shapes to static meshes by exposing the MeshInstance in the editor.
    • Allows instant loading at runtime
    • Could use a similar approach like collisions, where the corresponding MeshInstance is always exposed in editor with a setting when to regenerate

#76 proposes to use MeshInstances as base class for SS2D_Shape_Render instead of using the _draw() function, but otherwise keeps everything the same. We could actually do this as a quick first version to get the mentioned advantages (not the performance improvements) pretty much for free without much effort. However, it would generate several rendering nodes instead of just one when baking.

mphe avatar Aug 07 '24 20:08 mphe