Zylann / godot_heightmap_plugin

HeightMap terrain for Godot implemented in GDScript
Other
1.75k stars 159 forks source link

Symmetric Painting #465

Open Goutte opened 2 months ago

Goutte commented 2 months ago

Is your feature request related to a problem? Please describe.

We want to make an approximately symmetric map (RTS duel), not an exactly symmetric one. Easiest way is to allow this gorgeous addon to paint symmetries, on-demand.

Describe the solution you'd like

A brush (or filter, whatever) option to enable/disable symmetric painting.

Describe alternatives you've considered

Using three HTerrains:

It's clunky to have all three for the minimap (which is easy with just one HTerrain), navmesh (that one's OK in both cases) and fog of war (urgh).

And for spatialized game data passed to shaders as images for rendering or computation, it's simpler with one single HTerrain covering the whole map, instead of juggling with a mosaic.

Additional context

Managed to make a PoC :

image

func paint_input(center_pos: Vector2):
    _do_paint_input(center_pos)
    if _brush_symmetry == Symmetry.CENTRAL:
        _do_paint_input_when_available(_texture.get_size() - center_pos)

# This stunt was performed by a trained professional. Please do not try this at home.
func _do_paint_input_when_available(center_pos: Vector2):
    while true:
        await get_tree().process_frame
        if is_operation_pending():
            continue
        _do_paint_input(center_pos)
        break

Anyone also interested in symmetries ?

Perhaps with guidance I could clean this up ?

Zylann commented 2 months ago

A brush (or filter, whatever) option to enable/disable symmetric painting.

Symetric to what? I guess it would end up being restricted to specific full-terrain symetries. And of course, only two.

Doing this could be achieved using a duplicate painter in the painting system, integrate it to undo/redo properly, and show a second decal.

I'm not working often on this plugin nowadays, so I can't say when I'll have a look at implementing this.

MGilleronFJ commented 2 months ago

There is a problem that makes this harder to implement than it sounds: The painting system uses small viewports the size of the brush to process pixels on the GPU (while not requiring a high-end renderer, in theory). That already requires to setup on one frame and then wait the next frame to get the result. One could think we can just use an extra brush, just like we do already for multi-map editing (to edit multiple splatmaps at once) so we wouldn't have to wait extra frames (which would increase lag). The issue is that if two brushes edit the same map, both brushes will end up rendering on their own viewports, and therefore will not "see" each other's results since they render in parallel. So when the two brushes overlap, one will overwrite the other, causing rectangular artifacts the size of chunks used to keep undoable regions.

One solution would be to wait yet another frame to do the other brush, but that increases lag (halves the framerate at which the brush reacts), and introduces complications in the code which I would prefer not to go into. Using await is an endless source of potential timing bugs and I want to avoid it as much as possible. Another solution would be to change the brush system to use a full-terrain viewport, but that's obviously not scalable at all: rendering would take longer, take more memory, might not even work beyond some resolutions, and undo/redo would also take too much memory. Another solution would be for every brush shader to "include" the other brush by combining it to its shape, but that also sounds a bit complicated and annoying to update (all shaders would have to be modified just to support that). I also think it won't work with brushes that involve neighbor pixels (smoothing). If there was a way to paint on demand without having to wait a rendering frame, it could make things a bit easier, but despite the need being known for years Godot still doesn't have that. Also that would require a lot of refactoring.

So not sure what to settle on here.

Goutte commented 2 months ago

We needed central and mirror symmetry. We could also support custom pivot points, if we can ignore drawings outside of the canvas, which will happen with custom symmetry pivot points. (but that's not what's hard, if I understand correctly)

Using await is an endless source of potential timing bugs and I want to avoid it as much as possible.

100% agree ; it was a 30s hack that worked (barring undo), though, done after some hours of trying to make it work "properly". :)

Thanks for the rundown on the various potential approaches.

I understand my failed attempts much better.