Mesh Component

In this latest beta, we’ve added a quite exciting feature: the Mesh Component

Although the feature isn’t 100% done, we believe it’s still usable in its current form.
And we’d love to hear your feedback to this feature.

For instance, we lack proper documentation for this feature (but ofc that will come soon too).
So, currently, this test project will have to serve as a starting/learning point for those interested.

What does it do then, you ask?

Mesh

This component displays a single mesh.
This is different form a model which might have multiple meshes, and may be skinned.

Custom vertex formats

The mesh format supports custom vertex formats.
We use our buffer api which supports multiple named streams.
As such, you can get/set values and alter a stream at runtime. (E.g. positions, colors, normals…)

Formats

In order to make these files easy to generate and manipulate using tools or by hand, they’re in json format. (.mesh, .buffer)

Material

The named streams are automatically provided as input to the vertex shader.
There is no need to specify them in the material file itself.

Material Local Space

The data will be provided as-is to you in your shader, and you will have to transform vertices/normals as usual on the GPU.

Material World Space

If your material has “World” space set, you have to either provide a default “position” and “normal”, stream, or you can select it from the dropdown, when editing the mesh. This is so that the engine can transform the data to world space for batching with other objects.

Test project

MeshTest.zip (252.5 KB) (License: MIT)

Rotate the camera with the left moust button (or touching the screen)

Blender

Inside the test project, in assets/blender/ you’ll find the test scene I created for with geometry setup.

  • UV coordinates layer named texcoord0
  • Vertex color layer named color0
    (You can name these what you wish, as long as you match the names in your shaders)

Exporter

Also, you’ll find the defold_mesh.py, a Blender exporter which outputs to the mesh buffer format.
It is currently very basic, it currently only exports the mesh of the currently selected object

Currently, it supports positions, normals, uvcoordinates and vertex colors.

Wrap up

Finally, I’d also like to thank the team, most notably @sven and @mats.gisselson for their hard work implementing this feature. :clap:

And ofc a mention to @ross.grams for the excellent RenderCam which is part of this test project. :clap:

And please get back to us with feedback to this feature!

In this clip, I the manipulation of the buffer at runtime, using hot reload (CTRL+R) to refresh the resources while the app is still running.

(Update 2024-08-13: Added MIT license)

36 Likes

Can you give an example how to draw an arbitrary polygon at runtime?

5 Likes

Something like this? mesh_test.zip (21.2 KB)

The buffer resource here is empty, except the stream declaration, but the data is empty. At runtime I create a new buffer with the same stream declaration and fill it with data (a simple quad).

9 Likes

Exciting features and exciting times!

Who will be the first to make a voxel like world?

8 Likes

Epic Component! :defold::hugs:

7 Likes

Finally some long awaited 3D features!

6 Likes

Awesome work guys! Defold is just getting better and better.

3 Likes

Should every mesh cause a drawcall? I tried to optimize it but it seems like they always do?

Edit: Nevermind, material had local instead of world.

2 Likes

What’s wrong with this? Trying to create a dynamic cube from a triangle strip primitive. Sometimes the cube shows up right, other times vertices seem like they are out into infinity.

/gen/custom.script

local function fill_positions_strip(self, verts)

    for key, value in ipairs(verts) do
        self.positions[key] = verts[key]
    end

end

function init(self)
    
    self.res = go.get("#mesh", "vertices")
    print(self.res)
    
    self.buffer = resource.get_buffer(self.res)
    print(self.buffer)
    
    self.positions = buffer.get_stream(self.buffer, "position")
    print(self.positions)

    -- if the buffer has some data you could change these values here
    -- self.positions[1] = self.positions[1] + dt

    -- create a new buffer, since the one in the resource doesn't have enough size
    self.new_buffer = buffer.create(3 * 14, {
        { name = hash("position"),
         type=buffer.VALUE_TYPE_FLOAT32,
         count = 3 }
    })

    -- get the position stream
    self.positions = buffer.get_stream(self.new_buffer, "position")


    local verts2 = {
        0, 0, 0,
        0, 1, 0,
        1, 0, 0,
        1, 1, 0,
        1, 1, 1,
        0, 1, 0,
        0, 1, 1,
        0, 0, 1,
        1, 1, 1,
        1, 0, 1,
        1, 0, 0,
        0, 0, 1,
        0, 0, 0,
        0, 1, 0
    }


    fill_positions_strip(self, verts2)

    resource.set_buffer(self.res, self.new_buffer)

end

function update(self, dt)

end

MeshTest.zip (241.6 KB)

2 Likes

Since each stream specifies its component count, we specify the buffer size in elements.
The problem here is that you allocate a buffer with 3 * 14 elements.
And, if you don’t fill up the whole buffer, you’ll get garbage data, which may or may not show up in the renderer.

Here’s a fixed version.

local function fill_stream(stream, verts)
    for key, value in ipairs(verts) do
        stream[key] = verts[key]
    end
end

function init(self)
    self.res = go.get("#mesh", "vertices")
    
    self.buffer = resource.get_buffer(self.res)

    local verts2 = {
        0, 0, 0,
        0, 1, 0,
        1, 0, 0,
        1, 1, 0,
        1, 1, 1,
        0, 1, 0,
        0, 1, 1,
        0, 0, 1,
        1, 1, 1,
        1, 0, 1,
        1, 0, 0,
        0, 0, 1,
        0, 0, 0,
        0, 1, 0
    }

    -- create a new buffer, since the one in the resource doesn't have enough size
    self.new_buffer = buffer.create(#verts2 / 3, {
        { name = hash("position"), type=buffer.VALUE_TYPE_FLOAT32, count = 3 }
    })
    
    -- get the position stream
    self.positions = buffer.get_stream(self.new_buffer, "position")

    fill_stream(self.positions, verts2)

    resource.set_buffer(self.res, self.new_buffer)
end
8 Likes

Thank you! :innocent:

How would the same example be extended to have the normals, uvs, and colors?

I mean the part with buffer.create and then resource.set_buffer ?

local function fill_buffers_strip(self, verts)

    for key, value in ipairs(verts) do
        self.positions[key] = verts[key]
        self.normals[key] = verts[key]
    end

end

function init(self)
    
    self.res = go.get("#mesh", "vertices")

    -- unit square
    local verts2 = {
        0, 0, 0,
        0, 1, 0,
        1, 0, 0,
        1, 1, 0,
        1, 1, 1,
        0, 1, 0,
        0, 1, 1,
        0, 0, 1,
        1, 1, 1,
        1, 0, 1,
        1, 0, 0,
        0, 0, 1,
        0, 0, 0,
        0, 1, 0
    }

    
    self.buffer_position = buffer.create(#verts2 / 3, {
        { name = hash("position"),
         type=buffer.VALUE_TYPE_FLOAT32,
        count = 3 }
    })
    self.buffer_normal = buffer.create(#verts2 / 3, {
        { name = hash("normal"),
        type=buffer.VALUE_TYPE_FLOAT32,
        count = 3 }
    })
    self.buffer_texcoord0 = buffer.create(#verts2 / 3, {
        { name = hash("texcoord0"),
        type=buffer.VALUE_TYPE_FLOAT32,
        count = 2 }
    })
    self.buffer_color0 = buffer.create(#verts2 / 3, {
        { name = hash("color0"),
        type=buffer.VALUE_TYPE_FLOAT32,
        count = 4 }
    })    

    -- get the position stream
    self.positions = buffer.get_stream(self.buffer_position, "position")
    self.normals = buffer.get_stream(self.buffer_normal, "normal")


    fill_buffers_strip(self, verts2)

    resource.set_buffer(self.res, self.buffer_position)
    resource.set_buffer(self.res, self.buffer_normal)

end

function update(self, dt)

end

This is erroring with the below (referring to resource.set_buffer(self.res, self.buffer_normal)), what am I doing wrong?

ERROR:SCRIPT: /gen/custom.script:62: Could not copy data from buffer (9).
stack traceback:
	[C]: in function 'set_buffer'
	/gen/custom.script:62: in function </gen/custom.script:10>

Beyond that then we need a way to generate normals for custom meshes… or multiple ways with the different primitive types? There are for sure existing solutions out there…

1 Like

Each buffer can have multiple streams:

    self.new_buffer = buffer.create(num_vertices, {
        { name = hash("position"), type=buffer.VALUE_TYPE_FLOAT32, count = 3 },
        { name = hash("normal"), type=buffer.VALUE_TYPE_FLOAT32, count = 3 },
        { name = hash("texcoord0"), type=buffer.VALUE_TYPE_FLOAT32, count = 2 },
        { name = hash("color0"), type=buffer.VALUE_TYPE_FLOAT32, count = 4 }
    })

Remember to match this format with the format of the original (empty) buffer:

empty.buffer:

[
    {
        "name": "position",
        "type": "float32",
        "count": 3,
        "data": []
    },
    {
        "name": "normal",
        "type": "float32",
        "count": 3,
        "data": []
    },
    {
        "name": "texcoord0",
        "type": "float32",
        "count": 2,
        "data": []
    },
    {
        "name": "color0",
        "type": "float32",
        "count": 4,
        "data": []
    }
]

Here’s the patched code:

local function fill_stream(stream, verts)
    for key, value in ipairs(verts) do
        stream[key] = verts[key]
    end
end

function init(self)
    self.res = go.get("#mesh", "vertices")
    
    self.buffer = resource.get_buffer(self.res)

    local position = {
        0, 0, 0,
        0, 1, 0,
        1, 0, 0,
        1, 1, 0,
        1, 1, 1,
        0, 1, 0,
        0, 1, 1,
        0, 0, 1,
        1, 1, 1,
        1, 0, 1,
        1, 0, 0,
        0, 0, 1,
        0, 0, 0,
        0, 1, 0
    }

    local normal = {
        0, 0, 0,
        0, 1, 0,
        1, 0, 0,
        1, 1, 0,
        1, 1, 1,
        0, 1, 0,
        0, 1, 1,
        0, 0, 1,
        1, 1, 1,
        1, 0, 1,
        1, 0, 0,
        0, 0, 1,
        0, 0, 0,
        0, 1, 0
    }


    local texcoord0 = {
        0, 0,
        0, 1,
        1, 0,
        1, 1,
        1, 1,
        0, 1,
        0, 1,
        0, 0,
        1, 1,
        1, 0,
        1, 0,
        0, 0,
        0, 0,
        0, 1,
    }

    local color0 = {
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
        1,1,1,1,
    }

    local num_vertices = #position / 3

    -- create a new buffer, since the one in the resource doesn't have enough size
    self.new_buffer = buffer.create(num_vertices, {
        { name = hash("position"), type=buffer.VALUE_TYPE_FLOAT32, count = 3 },
        { name = hash("normal"), type=buffer.VALUE_TYPE_FLOAT32, count = 3 },
        { name = hash("texcoord0"), type=buffer.VALUE_TYPE_FLOAT32, count = 2 },
        { name = hash("color0"), type=buffer.VALUE_TYPE_FLOAT32, count = 4 }
    })
    
    -- get the position stream
    local stream_position = buffer.get_stream(self.new_buffer, "position")
    local stream_normal = buffer.get_stream(self.new_buffer, "normal")
    local stream_texcoord0 = buffer.get_stream(self.new_buffer, "texcoord0")
    local stream_color0 = buffer.get_stream(self.new_buffer, "color0")

    fill_stream(stream_position, position)
    fill_stream(stream_normal, normal)
    fill_stream(stream_texcoord0, texcoord0)
    fill_stream(stream_color0, color0)

    resource.set_buffer(self.res, self.new_buffer)
end
9 Likes

After playing with this for a bit, I have a bit of feedback:

  1. This solved this issue of creating geometry at runtime, but that geometry is still not clickable. For that to happen, we’d need to be able to create collision shapes at runtime. The buffer API sounds like a great use-case for that as well.
  2. I couldn’t get Material World Space to work. It looks like it completely disregards the world transform and everything gets rendered at (0, 0) in world space.
  3. Not a big deal, but it seems like I couldn’t get a bunch of meshes with the same material, uniforms and transform (but different buffers) to batch.
6 Likes

For 3 to work, I think the material vertex space needs to be world instead of local. Then the vertex position needs to be setup differently. Would still be nice for us to be shown the ideal way of doing this.

Yes, but I couldn’t get Material World Space to work. I selected the right streams for position and normal in the mesh component in the editor, and switched the vertex space to World Space in the material. And it seems like it completely discards the world transform on anything that I render, as if it’s sending the data in the buffer without transforming it.

And anyway, even in this state, I still get one render call per mesh.

1 Like

Re 2) When you use World Space, all vertices are transformed on the CPU, thus the world matrix for that mesh essentially becomes the identity matrix. It sounds like what you expected, but didn’t get, so I’ll take a look and try to create an example for you.

Re 3) The batching mechanism triggered only when using World Space. When using “Local Space”, each draw call uses a separate (unique) world transform, and thus cannot be batched. For that to happen, we’d have to implement instancing, which we currently haven’t had in the road map.
I added #4818 for instancing, and #4819 for physics custom meshes using the buffer format.

6 Likes

Yay, and now the links actually make sense to the community as well!

6 Likes

I remember noting that at one point the Release Notes switched over to point towards a private github repo, which means that the older links that didn’t work previously will now work for everyone!

This means that people can even go back to older release notes if they are interesting in how something was fixed :grin: (Looked quickly and it seem to be anything after and including 1.2.163

5 Likes

I have an idea for a new Defold library: functions for transforming 2d primitives into each other. Like circle -> square, square -> triangle and so on
Just if somebody has time to play with math

3 Likes

Hi, awesome feature!
I’m trying to play with your example for studing purposes:

I’ve a bit modified a shader to render textures with alpha channel.
Now I try to fix a mistake with semi-transparent quad (dice shadow), when his Z-coord is behind Z of the ground this quad renders wrong. I get an advice to modify render script with new predicate for transparent materials.

Sources: mesh.zip (313.3 KB)

Questions:

Thanks!

11 Likes