Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly document Mesh.surface_update_region #3787

Open
clayjohn opened this issue Jul 14, 2020 · 12 comments
Open

Properly document Mesh.surface_update_region #3787

clayjohn opened this issue Jul 14, 2020 · 12 comments
Labels
area:class reference Issues and PRs about the class reference, which should be addressed on the Godot engine repository enhancement

Comments

@clayjohn
Copy link
Member

The documentation for surface_update_region() currently says not to use it because it can be quite dangerous. While that is true, it would be a lot less dangerous to use if we actually documented how it works. The benefit of surface_update_region() is that it updates the OpenGL vertex buffer array directly. This saves a lot of overhead in the VisualServer, plus you don't have to delete and re-allocate buffers every frame. Accordingly, it is the fastest way to update meshes dynamically, but it is very complex and it is easy to completely break your mesh

Here I will lay out plans for updating the documentation appropriately. This is more for me than anyone else.

First: update Mesh.surface_update_region() and the corresponding VisualServer Functions.

Next: Add a tutorial to the Procedural Geometry series using Mesh.surface_update_region(). Mesh.surface_update_region() is the fastest way to have dynamic geometry. It is used internally by softbody meshes and Sprite3Ds. But it can be a real pain to work with. The tutorial will have to outline how to access the vertex buffer properly. As well as explain how each part of the buffer changes when mesh compression settings are used (since GDScript doesn't have half_float's this section will only be useful for GDNative and maybe C#).

Below is example code for the extreme basics.

extends MeshInstance

var array
var surface_format
var vertex_len
var index_len
	
var mesh_buffer
var mesh_stride
var vertex_offset

func _ready():
	array = VisualServer.mesh_surface_get_array(mesh.get_rid(), 0)
	surface_format = VisualServer.mesh_surface_get_format(mesh.get_rid(), 0)
	vertex_len = VisualServer.mesh_surface_get_array_len(mesh.get_rid(), 0)
	index_len = VisualServer.mesh_surface_get_array_index_len(mesh.get_rid(), 0)
	
	mesh_buffer = VisualServer.mesh_surface_get_array(mesh.get_rid(), 0)
	mesh_stride = VisualServer.mesh_surface_get_format_stride(surface_format, vertex_len, index_len)
	vertex_offset = VisualServer.mesh_surface_get_format_offset(surface_format, vertex_len, index_len, VisualServer.ARRAY_VERTEX)

func _process(delta):
	var pos = get_node("../Camera").project_position(get_viewport().get_mouse_position(), 1.5)
	var a = PoolRealArray()
	a.resize(3)
	a[0] = pos.x
	a[1] = pos.y
	a[2] = pos.z
	var c = var2bytes(a)

	# Ignore the array type and the var type inside the array
	for i in range(12):
		array[vertex_offset + i] = c[i+8]
	
	VisualServer.mesh_surface_update_region(mesh.get_rid(), 0, 0, array)
@clayjohn
Copy link
Member Author

After profiling. It turns out that the use of PoolRealArray and var2bytes() is so slow that it washes out all the benefit of using mesh_surface_update_region(). Accordingly, it ends up being slower to use this method. I will still document it as it is useful for others working on the engine, custom modules, GDNative, and maybe C#. However, I won't add a tutorial.

@doko-desuka
Copy link

Accordingly, it ends up being slower to use this method.

What other methods of changing geometry from a script on each frame are there?

@Calinou
Copy link
Member

Calinou commented Jul 31, 2020

@doko-desuka You can use ImmediateGeometry (which is slow with hundreds of vertices or more) or shaders (which are the fastest way, but with no communication from the GPU to the CPU).

@clayjohn
Copy link
Member Author

Also see the procedural geometry tutorial which has a full discussion.

@doko-desuka
Copy link

@clayjohn thanks for the link. I can see that ArrayMesh is really what I'm after, being able to submit a list of vertices at once so it's faster than submitting each individual element.

But there's no apparent way to update the arrays used by ArrayMesh? (this issue is proposing to remove the current way, surface_update_region(), in favor of a new better way)

@clayjohn
Copy link
Member Author

clayjohn commented Aug 2, 2020

@doko-desuka The current way is to use add_surface_from_arrays the way it is done in the tutorial I linked.

surface_update_region() is a different way of updating the mesh, but it is not properly documented. This issue is just a reminder for myself to document surface_update_region() because I finally learned how to use it properly.

@doko-desuka
Copy link

@clayjohn understood, thanks!
Just for reference, it looks like that Dragonbones framework is using something different in their Godot plugin, with VisualServer.canvas_item_add_triangle_array() as seen here:
https://github.com/sanja-sa/gddragonbones/blob/master/gddragonbones/src/GDMesh.h#L41-L53
...and then updating the arrays in here:
https://github.com/sanja-sa/gddragonbones/blob/master/gddragonbones/src/GDSlot.cpp#L353-L357

@clayjohn
Copy link
Member Author

clayjohn commented Aug 2, 2020

@doko-desuka Thanks for the link! That is a little different from what is being discussed here. What you have linked there is also how the Polygon2D node works. It doesn't involve meshes at all, it uses the VisualServer API to issue a canvas item command.

Keep in mind, this issue only relates to updating ArrayMeshes dynamically using the surface_update_region() function.

@doko-desuka
Copy link

doko-desuka commented Aug 2, 2020

@clayjohn Ah I see, thanks for the details.

It would be interesting to test later on if a C# script has that same overhead problem from your comment above, about PoolRealArray and var2bytes().
According to this part of the docs, a byte array from C# can be used in place of a PoolByteArray? If so, you could have a float array where you keep the vertex positions that you might change on each frame, and when it's time for the mesh_surface_update_region() call, you do a C# Buffer.BlockCopy() from the float array to the byte array to be sent to the function (BlockCopy example in here: https://stackoverflow.com/a/4636735).

Since I'm interested in having a 2D deforming mesh, I'll see if I can compare this C# array method with that VisualServer.canvas_item_add_triangle_array method from Dragonbones (assuming _add_triangle_array is accessible to C# scripts).

@clayjohn
Copy link
Member Author

clayjohn commented Aug 2, 2020

@doko-desuka I'd be very interested to hear about how it performs in C#. In C++ it is significantly faster to use surface_update_region(). In GDScript the only reason it isn't faster is because of the painful conversion to bytes. I believe C# should be very fast.

@NathanLovato NathanLovato added the area:class reference Issues and PRs about the class reference, which should be addressed on the Godot engine repository label Sep 27, 2020
@clayjohn
Copy link
Member Author

clayjohn commented Apr 9, 2021

Note to self (and others): I will come back to this after 4.0 releases. Currently this group of functions is not worth using from GDScript in 3.x. However, with the updates to GDScript in 4.0 and the changes to the rendering API, this documentation will be very helpful.

In particular see godotengine/godot#47761

@AFE-GmdG
Copy link

AFE-GmdG commented Jul 3, 2021

I'm using C# and in there a MemoryStream/BinaryWriter combination to update my Vertices. That seems reasonably fast to me and I don't have to rebuild the arrays each frame:

using System.IO;
using System.Text;
using Godot;

namespace VR.Scenes.Tests
{

    [Tool]
    public class DynamicRing : Spatial
    {

        private int _segments = 16;
        private float _radius = 0.5f;
        private bool _rebuildNecessary = false;

        private MeshInstance _mi;

        private RID _meshRid;
        private byte[] _array;
        private uint _surfaceFormat;
        private int _vertexLen;
        private int _indexLen;
        private int _vertexStride;
        private BinaryWriter _writer;

        [Export(PropertyHint.Range, "2, 64, 1, or_greater")]
        public int Segments
        {
            get => _segments;
            set
            {
                _segments = Mathf.Max(2, Mathf.Min(1024, value));
                _rebuildNecessary = true;
            }
        }

        [Export(PropertyHint.Range, "0.01, 4.0f, 0.01")]
        public float Radius
        {
            get => _radius;
            set
            {
                _radius = Mathf.Max(0.01f, Mathf.Min(4.0f, value));
                _rebuildNecessary = true;
            }
        }

        public override void _Ready()
        {
            base._Ready();
        }

        public override void _EnterTree()
        {
            base._EnterTree();
            if (_mi == null)
            {
                _mi = new MeshInstance();
                _mi.Name = "DebugGridMeshInstance";
                _mi.Mesh = new ArrayMesh();
                _rebuildNecessary = true;

                AddChild(_mi, true);
            }
        }

        public override void _ExitTree()
        {
            base._ExitTree();
            if (_mi != null)
            {
                RemoveChild(_mi);
                _mi.QueueFree();
                _mi = null;
            }
            if (_writer != null)
            {
                _writer.Close();
                _writer.Dispose();
                _writer = null;
            }
        }

        private float time = 0;
        public override void _Process(float delta)
        {
            base._Process(delta);

            var mesh = _mi.Mesh as ArrayMesh;
            if (mesh == null)
                return;

            if (_rebuildNecessary)
            {
                if (_writer != null)
                {
                    _writer.Close();
                    _writer.Dispose();
                    _writer = null;
                }

                while (mesh.GetSurfaceCount() > 0)
                    mesh.SurfaceRemove(0);

                var vertices = new Vector3[_segments * 2];

                for (var i = 0; i < _segments; ++i)
                {
                    float phi = i * Mathf.Tau / _segments;
                    float x = Mathf.Cos(phi) * _radius;
                    float z = Mathf.Sin(phi) * _radius;
                    vertices[i * 2 + 0] = new Vector3(x, 0.0f, z);
                    vertices[i * 2 + 1] = new Vector3(x, 0.2f, z);
                }

                var a = new Godot.Collections.Array();
                a.Resize((int)ArrayMesh.ArrayType.Max);
                a[(int)ArrayMesh.ArrayType.Vertex] = vertices;
                mesh.AddSurfaceFromArrays(ArrayMesh.PrimitiveType.Lines, a, null, 0);

                _rebuildNecessary = false;

                _meshRid = mesh.GetRid();
                _array = VisualServer.MeshSurfaceGetArray(_meshRid, 0);
                _surfaceFormat = VisualServer.MeshSurfaceGetFormat(_meshRid, 0);
                _vertexLen = VisualServer.MeshSurfaceGetArrayLen(_meshRid, 0);
                _indexLen = VisualServer.MeshSurfaceGetArrayIndexLen(_meshRid, 0);
                _vertexStride = (int)VisualServer.MeshSurfaceGetFormatStride(_surfaceFormat, _indexLen, _indexLen);

                _writer = new BinaryWriter(new MemoryStream(_array, true), Encoding.ASCII, false);
            }


            time += delta;
            var y = 0.7f + Mathf.Sin(time) * 0.5f;

            // Update some vertices each frame:
            for (var i = 0; i < _segments; i += 2)
            {
                // MeshSurfaceGetFormatOffset seems not to be implemented and alwasy return 0 :(
                // var vertexOffset = VisualServer.MeshSurfaceGetFormatOffset(_surfaceFormat, _vertexLen, _indexLen, i * 2 + 1);

                // Testcalculation of updated Vertex: Each other line should update the top vertex with y
                var index = (i * 2 + 1);

                // Security break: _vertexLen is the amount of vertices inside the _array
                if (index >= _vertexLen)
                    break;

                // calculate the byte offset of the vertex inside the _array:
                // _vertexStride is the size in bytes of a single vertex.
                int vertexOffset = _vertexStride * index;

                // Write the second component of the vertex. A float is 4 bytes - so add 4 to the offset (VertexPosition.Y)
                _writer.Seek(vertexOffset + 4, SeekOrigin.Begin);

                // Write the updated value
                _writer.Write(y);
            }

            _writer.Flush();
            mesh.SurfaceUpdateRegion(0, 0, _array);

        }

    }

}

For me this solution seems to work perfectly

To use this script create a Scene with a Spacial node and attact this script to the it.
1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:class reference Issues and PRs about the class reference, which should be addressed on the Godot engine repository enhancement
Projects
None yet
Development

No branches or pull requests

5 participants