Performance impact on thousands of Nodes

Sorry to interfere, but if we are talking about the style of minecraft physics of the blocks in this game does not exist. This is just a vertex-level animation, so you don’t need to divide the vertices into separate models to manage them, you can recalculate them directly in memory.

I’m not sure if I understood your statement correctly. I think the falling block animation would be more efficient if it is it’s own separate model, instead of updating the entire model containing multiple blocks multiple times for whatever frames it takes the block to fall down.

Hi,

If you’re intent on using the flattenStrong method and things like that, you could have 2 copies: one would be the collection of blocks in your “chunk” when they aren’t flattened, the other would be your flattened model. When nothing is being modified, show your flattened model and hide your collection of blocks. When something is being modified, swap to showing the collection of single blocks and after the modification is done, destroy your old flattened model and create a new one by re-flattening your collection of blocks in their updated state. Then after the modification is done, hide your block-collection again and show the flattened-model. Swapping back and forth like that could work for things like simulating animations…but obviously it might make things sluggish if you’re dealing with thousands of blocks.

This is just an idea, but the best way to implement a minecraft-like world with many blocks would be to deal directly with low-level geometry as others have suggested on this thread.

1 Like

Hi,

I’m a bit curious about whether this increase in efficiency involves significant speed increases? Does using memoryviews lead to faster geometry-manipulation as compared to using GeomVertexReaders/Writers? If so, is it by a significant margin?

Yes, it is very much possible. In fact, this might be another good reason to not use flattenStrong, as it isn’t obvious what indices the vertices will end up having within the flattened geometry (might depend on the order of the children of the flattened node, but I haven’t checked).

Anyway, the solution is to associate the start of the block’s vertex data with an index in some list (in my code sample it’s the index of its position in a list of predetermined positions). Then you copy that vertex data to a new Geom (and GeomPrimitive) in a new GeomNode.

The code sample has been edited to include a prepare_separated_cube method that sets up these new structures, which are then filled with the separated cube’s vertex data. There’s also a __drop_cubes task now, which lowers the z-value of the separated cube(s) every frame by a constant amount until a certain value is reached, upon which the node is detached from the scenegraph.
A bit simplistic, but it’s just an example.

The separated cubes have been given a red color to differentiate them from the others.

That question is best answered in this blog post, I guess :slight_smile: .
It does take a little getting used to (memoryviews are quite rigid, in that you can’t change their size, so you need to make sure that the data you create a view of already has the correct size), but I do find it worth the effort to learn how to use them.

And using GeomVertexArrayDataHandle and its methods is an interface that is being discouraged in favor of memoryview usage as well.
Let me know if you need any help with that :wink: .

Well, if the advantages of using them far outweigh those of using whatever has been written in the manual and the api as well, I think those into procedural geometry creation and manipulation would benefit from a tutorial that covers:

  • The best way to create geometry procedurally using memoryviews.
  • The best way to extend the existing geometry of a model using memoryviews.
  • The best way to delete the existing geometry of a model using memoryviews.

If a clear and concise tutorial were written by someone competent and experienced with memoryviews that covers those 3 things, then I think we would all benefit from it. Not to put all that on your shoulders of course… :sweat_smile:

My attempt at doing so can be found here :wink: :

instead of making use of GeomVertexWriters, my version uses memoryviews to access vertex data.
To make things more interesting, I’ve made it possible to add and delete any of the cube’s sides at runtime by direct vertex data manipulation.

Perhaps it could still be improved; when I have some time I should have a look at it again.

Oh yeah, I remember those. I meant something more akin to a new manual/api extension that documents the same.

I’m sure in this past year your skills have greatly improved and so, if you look at it again, it would be more than a work of art. :+1:

Yeah, that might be a good idea - again, when I get some time :sweat_smile:. And if the devs approve :grin:.

Again, thank you very much. You and some other people are the kind of person that is single-handedly making the community great. I really appreciate the time and effort you contribute into this community.

I have a few questions:

  1. can you point towards a manual of texture atlas in blender? Tried googling it but didn’t find anything useful.

  2. Is the most efficient way of adding a very simple cracking animation(4 frames) adding 4 more textures for each kind of block? Or is it possible to add the cracking animation on top of the original texture since it is the same for every kind of block? Is it the multi-texture thing?

  3. more of a clarification, but looking at your code, you are using memoryview to combine the internal values of the geom, is that correct?

  4. I’m not sure if I’m understanding your code correctly, but how come prepare_seperated_cube is not taking an input for the index? Is it just creating a new cube?

I meant it.

from panda3d.core import *
from random import randint
from direct.task.Task import Task
from direct.showbase.ShowBase import ShowBase

class MyApp(ShowBase):


    def __init__(self):
        ShowBase.__init__(self)

        # Create
        node = GeomNode('gnode')
        geoms = []
        textures_count = 3

        for i in range(textures_count):
            gvd = GeomVertexData('name', GeomVertexFormat.getV3t2(), Geom.UHStatic)
            geom = Geom(gvd)
            prim = GeomTriangles(Geom.UHStatic)
            vertex = GeomVertexWriter(gvd, 'vertex')
            texcoord = GeomVertexWriter(gvd, 'texcoord')
            tex = loader.loadTexture('%i.png' % (i+1))
            tex.setMagfilter(Texture.FTLinearMipmapLinear)
            tex.setMinfilter(Texture.FTLinearMipmapLinear)
            geoms.append({'geom':geom,
                          'prim':prim,
                          'vertex':vertex,
                          'texcoord':texcoord,
                          'index':0,
                          'gvd':gvd,
                          'texture':tex})

        for x in range(0,100):
            for z in range(0,100):
                t_img = randint(0,textures_count - 1)
                i = geoms[t_img]['index']
                geoms[t_img]['vertex'].addData3f(x, 0, z)
                geoms[t_img]['texcoord'].addData2f(0, 0)
                geoms[t_img]['vertex'].addData3f(x, 0, z+1)
                geoms[t_img]['texcoord'].addData2f(0, 1)
                geoms[t_img]['vertex'].addData3f(x+1, 0, z+1)
                geoms[t_img]['texcoord'].addData2f(1, 1)
                geoms[t_img]['vertex'].addData3f(x+1, 0, z)
                geoms[t_img]['texcoord'].addData2f(1, 0)
                d = i * 4
                geoms[t_img]['prim'].addVertices(d, d + 2, d + 1)
                geoms[t_img]['prim'].addVertices(d, d + 3, d + 2)
                geoms[t_img]['index'] += 1

        for i in range(textures_count):
            geoms[i]['prim'].closePrimitive()
            geoms[i]['geom'].addPrimitive(geoms[i]['prim'])
            node.addGeom(geoms[i]['geom'])
            node.setGeomState(i, node.getGeomState(i).addAttrib(TextureAttrib.make(geoms[i]['texture'])))

        terrain = render.attachNewNode(node)
        
        del geoms
        
        # Modify
        taskMgr.add(self.animation,'animation')
        
        geom_blue = node.modifyGeom(2)
        self.vdata = geom_blue.modifyVertexData()

    def animation(self, task):

        vertex_blue = GeomVertexRewriter(self.vdata, 'vertex')
        
        vertex_blue.setRow(0)
        v = vertex_blue.getData3f()
        vertex_blue.setData3f(v[0], v[1]-0.05*globalClock.get_dt(), v[2])
        
        vertex_blue.setRow(1)
        v = vertex_blue.getData3f()
        vertex_blue.setData3f(v[0], v[1]-0.05*globalClock.get_dt(), v[2])
        
        vertex_blue.setRow(2)
        v = vertex_blue.getData3f()
        vertex_blue.setData3f(v[0], v[1]-0.05*globalClock.get_dt(), v[2])
        
        vertex_blue.setRow(3)
        v = vertex_blue.getData3f()
        vertex_blue.setData3f(v[0], v[1]-0.05*globalClock.get_dt(), v[2])
        
        return Task.cont

app = MyApp()
app.run()

1 2 3
1.png, 2.png, 3.png

I set the size of the world:

for x in range(0,1000):
    for z in range(0,1000):

And got an FPS of 65, i think the implementation in C++ will increase the speed.

1 total nodes (including 0 instances); 0 LODNodes.
0 transforms; 0% of nodes have some render attribute.
3 Geoms, with 3 GeomVertexDatas and 1 GeomVertexFormats, appear on 1 GeomNodes.
4000000 vertices, 0 normals, 0 colors, 4000000 texture coordinates.
GeomVertexData arrays occupy 78125K memory.
GeomPrimitive arrays occupy 23438K memory.
2000000 triangles:
  0 of these are on 0 tristrips.
  2000000 of these are independent triangles.
3 textures, estimated minimum 192K texture memory required.
1 Like

Not sure what I did wrong but the code you provided only gives me a grey screen. I put the textures under the same folder as the code.

Hold down the right mouse button and pull it away from you. Use the pinched wheel for viewing.

不客气 :slight_smile: . It is because of people like you who appreciate the help they receive that I am motivated to keep helping out :wink: .

What I’ve read about Blender 2.8 (I don’t really use Blender myself) is that it’s now possible to select multiple objects in object mode and then go to edit mode and unwrap all of them at once.

But since the textures are just squares (one for each block side), I think it should actually be easy to use a paint program like Gimp to create the atlas manually. You could copy and paste all of the needed square textures side-by-side into one texture (the atlas) big enough for all of them (something like 1024 x 1024 would be good). Fill the width and height of the atlas with an integer number of block side textures, so that e.g. 8 of them take up the entire width or height; this makes it easier to calculate the UV coordinates needed for the model vertices.

To know where (and in what orientation) to place each side texture, you can implement an algorithm that offsets each side of a block (and provides an additional offset for the different types of blocks as well) and let it print out the UVs it computes. My code sample already does something like that (at least for the sides, whose textures are expected to be placed left-to-right like this: left, back, bottom, right, front and top, where bottom and top might need to be rotated to suit your preference), but you’ll need to adapt it to take the number of different block types into account.

This will probably be the most time-consuming part of your initial development, but I’d still do it manually rather than using Blender and then loading and analyzing the exported model, but that’s your choice.

If your animation frames only depict the cracks themselves, then you can definitely use multitexturing to blend them with the main block texture. For this purpose, the background should be white; then you can use the default modulate blend mode to make the main block texture show through the animation frames.

To play the animation, it will probably suffice to call set_texture (using the TextureStage associated with the “top texture”) on the block nodes 4 times, using a different animation frame each time. The alternative would be to use a 3-D texture, but I’m not sure it’s worth creating 3-D UV coordinates for such a short animation.

What I would like to know, is if the animation is used to show a gradual decay of the block (i.e. each time the player hits the block, the animation advances one frame), or if it plays from start to finish without delay and is immediately followed by the block falling down?
More specifically, can it happen that multiple blocks need to be animated at the same time (perhaps even with each block showing a different frame)? That would make things a bit more complicated, requiring the entire chunk model to use multitexturing and adjusting the UVs for specific vertices.

If only one block animates at any given time, you could just separate that block first, set up multitexturing for it and then play the animation on it.

Yes, that is correct.

It is indeed only creating a new cube node; it is merely a helper method, whose cube creation code was split off from remove_cube, to prevent the code from becoming too cluttered.

Even though I couldn’t get it to work(only worked once but I’ll try to figure it out), it seems like a really good idea of yours not to bother to separate the mesh. The question is will the texture on the side that was hidden still show? Thank you.

#1 If I understood correctly, I need to draw in 2d, put it all the textures in a big square? Then make an algorithm that knows where the appropriate textures are for every side of each block? If I could, instead an algorithm, just store all the coordinates in a giant dictionary? Because I still can’t quite understand this part of the code, not to say add more complexity:

                pos = Point3()
                pos[i] = direction
                pos[(i + direction) % 3] = a
                pos[(i + direction * 2) % 3] = b
                u, v = [pos[j] for j in range(3) if j != i]
                u *= (-1. if i == 1 else 1.) * direction
                uv = (max(0., u) / 6. + u_offset, max(0., v))

I understand it’s trying to add texture offsets but what is “pos”? If you could explain that part of code, that would be great!

Also, (stupid question) where am I supposed to add the texture to geom after loading the texture?

#2 The cracking animation needs to be finished start to finish or else it needs to start over. There wouldn’t be any time that multiple blocks needing to be animated the cracking animation. However, there could be multiple animations happening at the same time, i.e. multiple fires.

#3 ok, thank you.

#4 So if it is creating a new cube, does the original chunk model need to delete the original cube?

In fact, it was not hidden, the tile changes position, rising above the geometry. It’s strange to me why you can’t make it work.

Modified, there is a camera preset.

from panda3d.core import *
from random import randint
from direct.task.Task import Task

loadPrcFileData("editor-startup", "show-frame-rate-meter #t")
loadPrcFileData("editor-startup", "sync-video #f")

from direct.showbase.ShowBase import ShowBase

class MyApp(ShowBase):


    def __init__(self):
        ShowBase.__init__(self)
        
        self.trackball.node().set_pos(0, 30, 0)
        self.trackball.node().set_p(-45)

        # Create
        node = GeomNode('gnode')
        geoms = []
        textures_count = 3

        for i in range(textures_count):
            gvd = GeomVertexData('name', GeomVertexFormat.getV3t2(), Geom.UHStatic)
            geom = Geom(gvd)
            prim = GeomTriangles(Geom.UHStatic)
            vertex = GeomVertexWriter(gvd, 'vertex')
            texcoord = GeomVertexWriter(gvd, 'texcoord')
            tex = loader.loadTexture('%i.png' % (i+1))
            tex.setMagfilter(Texture.FTLinearMipmapLinear)
            tex.setMinfilter(Texture.FTLinearMipmapLinear)
            geoms.append({'geom':geom,
                          'prim':prim,
                          'vertex':vertex,
                          'texcoord':texcoord,
                          'index':0,
                          'gvd':gvd,
                          'texture':tex})

        for x in range(0,100):
            for z in range(0,100):
                t_img = randint(0,textures_count - 1)
                i = geoms[t_img]['index']
                geoms[t_img]['vertex'].addData3f(x, 0, z)
                geoms[t_img]['texcoord'].addData2f(0, 0)
                geoms[t_img]['vertex'].addData3f(x, 0, z+1)
                geoms[t_img]['texcoord'].addData2f(0, 1)
                geoms[t_img]['vertex'].addData3f(x+1, 0, z+1)
                geoms[t_img]['texcoord'].addData2f(1, 1)
                geoms[t_img]['vertex'].addData3f(x+1, 0, z)
                geoms[t_img]['texcoord'].addData2f(1, 0)
                d = i*4
                geoms[t_img]['prim'].addVertices(d, d + 2, d + 1)
                geoms[t_img]['prim'].addVertices(d, d + 3, d + 2)
                geoms[t_img]['index'] += 1

        for i in range(textures_count):
            geoms[i]['prim'].closePrimitive()
            geoms[i]['geom'].addPrimitive(geoms[i]['prim'])
            node.addGeom(geoms[i]['geom'])
            node.setGeomState(i, node.getGeomState(i).addAttrib(TextureAttrib.make(geoms[i]['texture'])))

        terrain = render.attachNewNode(node)
        #terrain.analyze()
        
        del geoms
        
        # Modify
        taskMgr.add(self.animation,'animation')
        
        geom_blue = node.modifyGeom(2)
        self.vdata = geom_blue.modifyVertexData()

    def animation(self, task):

        vertex_blue = GeomVertexRewriter(self.vdata, 'vertex')
        
        vertex_blue.setRow(0)
        v = vertex_blue.getData3f()
        vertex_blue.setData3f(v[0], v[1]-0.1*globalClock.get_dt(), v[2])
        
        vertex_blue.setRow(1)
        v = vertex_blue.getData3f()
        vertex_blue.setData3f(v[0], v[1]-0.1*globalClock.get_dt(), v[2])
        
        vertex_blue.setRow(2)
        v = vertex_blue.getData3f()
        vertex_blue.setData3f(v[0], v[1]-0.1*globalClock.get_dt(), v[2])
        
        vertex_blue.setRow(3)
        v = vertex_blue.getData3f()
        vertex_blue.setData3f(v[0], v[1]-0.1*globalClock.get_dt(), v[2])
        
        return Task.cont

app = MyApp()
app.run()