Performance impact on thousands of Nodes

I tried again with a mouse and got it to work. But is it intended to be 2d? I can’t see the animation as well.

In fact, there is no difference between a flat geometry in the form of tiles or a cube. To be honest, I’m not going to implement mincraft as an example. :slightly_smiling_face:

Keep an eye on the first blue tile in the row.

1 Like

Nice! Again, thank you so much.

Yes, a texture atlas combines all of the different textures needed for the blocks into one big texture.

It might indeed be easier (especially in the beginning, when figuring out how it all works) to hard-code the UV-coordinates instead of using an algorithm that may look a bit “magical” :stuck_out_tongue: . The reason I use it is mainly to keep the code more compact, but you might prefer to write out the vertex data more explicitly, separately for each side of a block.

“pos” is the vertex position (x-, y- and z-coordinates), relative to the origin of the block. It is from this position that I derive the texture (UV) coordinates. Don’t worry if their relationship isn’t all that clear, since a different algorithm would be needed anyway (but like you suggested, it may be easier to just set the UVs to specific values that you get by dividing the pixel coordinates in the atlas by the size of the atlas).

You can add the texture to the NodePath that contains the “chunk” geometry (in my code sample it’s just self.model, so you call self.model.set_texture(my_texture_atlas)).

That’s OK then…

…but that could be problematic. It’s not obvious to me how different parts of one and the same geometry can have different animations applied to them, hmmm… Perhaps we shouldn’t worry about this yet and just take it one step at a time.

That’s not strictly necessary, no. The new cube will simply be a copy of the original one. In remove_cube, you can leave out the following lines:

        model_view[start:-cube_data_size] = model_view[end:]
        model_array.set_num_rows(model_vert_count - self.cube_vert_count)

and:

        model_prim_view[start:-self.cube_prim_vert_count] = model_prim_view[end:]
        prim_array.set_num_rows(model_prim_vert_count - self.cube_prim_vert_count)

The original cube will remain part of the chunk model then.

…but that could be problematic. It’s not obvious to me how different parts of one and the same geometry can have different animations applied to them, hmmm… Perhaps we shouldn’t worry about this yet and just take it one step at a time.

Well, maybe each block with animation would be a seperate mesh. Such as fire would be it’s own mesh.

That’s not strictly necessary, no. The new cube will simply be a copy of the original one. In remove_cube, you can leave out the following lines:

Then won’t there be 2 of the same model to be seen?

OK, that should work.

Yes, what I meant was that you can do whatever you want with the original cube geometry within the chunk. You can remove it or leave it in, it’s up to you. It’s independent from the newly created cube node, which simply contains a copy of that original geometry.

Last question: what is uv? Is it the texture offset thing? Why is it giving 4 coordinates per face? I’m also assuming that the texture atlas are being scaled to 1x1.

Once again, thank you very much, not gonna repeat my appreciation message again :sweat_smile:. BTW how did you find out I understand Chinese.

This is explained quite well here and here; it might also help to read about texels. One thing to note about the relationship between a texel and its UV coordinates is that the vertical coordinate of a texel is usually measured from top to bottom in a paint program, while the corresponding texture coordinate (V) is measured from bottom to top. So the texel at (512, 256) in a 1024x1024 image will have its UVs at (0.5, 0.75).

The texture offset in my code just allows all six cube side textures to be placed side-by-side within the texture atlas.
Each face has four vertices, and each of those vertices needs to have different UVs.
Indeed, the UVs of the texture atlas range from 0 to 1, just like for any texture map.

Ah, in my recently upgraded crystal ball I saw you watching a Chinese drama without English subtitles :mage:!
Just kidding. Your username seemed like a good hint :wink: .

I probably should post this in Scripting section, but while we are here, I’m going to ask it anyway. Is it appropriate to use multiple ShowBases? More specifically, I am going to split the code into multiple classes, which all requires ShowBase to load texture. Thanks again!

Just pass self as an argument to the desired class.

from direct.showbase.ShowBase import ShowBase

class Test():
    def __init__(self, base_link):

        base_link.accept('enter', self.message)

    def message(self):
        print("test")

class MyApp(ShowBase):

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

        myClass = Test(self)

app = MyApp()
app.run()
1 Like
class A:
	def __init__(self):
		self.hi = 'hey'
		self.b = B(self)
class B:
	def __init__(self, global_self):
		self.global_self = global_self
	def check(self):
		print(self.global_self.hi)
a = A()
a.b.check()

Something like this? Why does this feel so trippy?

Thank you. I just thought of that inheritance should also work.

ShowBase creates a number of globally-accessible variables–including one called “base”, which holds a reference to the current ShowBase instance, and “loader”, which holds a reference to the current asset-loader. These variables should be available just about anywhere, meaning that you should in general not need multiple ShowBase instances in order to access things like texture-loading.

For example:

The game’s main Python file:

from CatFile import Cat

class MyGame(ShowBase):
    # Code for the class here...

myGame = MyGame()
myGame.run()

“CatFile.py”:

class Cat():
    def __init__(self):
        # "loader" should be available here, being a global variable
        self.texture = loader.loadTexture("someTexture.png")
1 Like

Thank you. That solves my problem very nicely.

However, I still need to access “self.render” in “Cat”, without making the “MyGame” parent of “Cat”. Because that would cause to spawn multiple ShowBase instances(I will create multiple "Cat"s).

1 Like

You don’t have to access “render” via a “self” variable, however: like “base” and “loader”, “render” is a globally-accessible variable provided by ShowBase. (Along with a number of others.)

That said, even if it weren’t, “base”–which references the current ShowBase instance–is another such globally-available variable. This means that code that wants to access some element of the current ShowBase instance can access it via “base.<whichever variable>”.

To illustrate, let me extend the example above:

The game’s main Python file:

from CatFile import Cat

class MyGame(ShowBase):
    def __init__(self):
        # I imagine that you could use "super" here; I'm just
        # more familiar with using "<SuperClass>.__init__(...)"
        ShowBase.__init__(self)
        
        # Now, let's add a variable to our class--something
        # that we may want to access later...
        self.meowText = "MEOW!"

myGame = MyGame()
myGame.run()

“CatFile.py”

class Cat():
    def __init__(self):
        # "loader" should be available here, being a global variable
        self.texture = loader.loadTexture("someTexture.png")

        self.model = loader.loadModel("someModel")
        # "render" should likewise be available here,
        # being a global variable
        self.model.reparentTo(render)

        # Let's say that we want to get the size of the current window...
        # We can access that size from the "win" variable in the 
        # current ShowBase instance, like so:
        winSize = base.win.getSize()

        # And now, let's say that we want to get that new
        # variable that we added to our main class, above.
        # Since our main class extends ShowBase, and since we
        # initialised an instance of it, that instance should be
        # the thing referenced by "base". As a result, we can
        # access our main game class in this way!
        meowVal = base.meowText
1 Like

@Thaumaturge This is of course a way out, but you need to understand that the logic is not built around the Panda engine. And to be honest, this is very strange and not transparent code. It’s easier to pass the self parameter. Of course, I know about the argument as a reference to a variable, but this approach requires remembering all the arguments as they are passed from module to module, after some time you can forget what any of them means. As I said, it is better to pass self, because any variable or reference may be required in the main module.

main.py

from direct.showbase.ShowBase import ShowBase
from catfile import Cat

class Main(ShowBase):

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

        self.status = True

        cat = Cat(self)

main = Main()
main.run()

catfile.py

class Cat():
    def __init__(self, main):

        self.main = main

        base.accept('enter', self.message)
        # Also a working option.
        # self.main.accept('enter', self.message)

    def message(self):
        if self.main.status:
            print("Meow")
            self.main.status = False
        else:
            print("Purr")

if self.main.status:
self.main.status = False

Please note how the request form looks, it is clear to you .

I see what you’re saying, I believe, but passing around parameters in that way can get very unwieldy in my experience. Especially in a larger project!

And furthermore, keeping such references in potentially a great many places incurs the risks of forgetting to clear them and of developing (likely multiple) cyclic references, which may result in memory leaks.

Personally, I’d prefer to stick to accessing the global variables. It’s only slightly less clear, and somewhat safer, and even a little cleaner, I feel.

As to remembering the various variables available, I find that I tend to remember the important ones, and the rest can just be looked up.

Or you could just do it the canonical way :wink: .

For example, what I tend to do is create an additional module, e.g. base.py, dedicated to the importing of Panda3D (and whatever else you might need) and the defining of classes, helper functions, constants and globals.
Then you can import that module in all of your other modules and it should work in the same way as with the global builtins.

For example:

base.py

# import panda3d
from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
# do some additional imports
import random


# define some constant values
CONSTANT1 = 0
CONSTANT2 = 1


# define a helper function
def helper_function():

    print("This is a helper function.")


# create the ShowBase instance and make it an attribute of this module, along
# with some of its instance variables and methods
showbase = ShowBase()
render = showbase.render
loader = showbase.loader
load_model = loader.load_model
load_tex = loader.load_texture
camera = showbase.camera
task_mgr = showbase.task_mgr


# Create some "global" variables;
# add them to a class or container-type object, like a list or dict, so values
# can be assigned to them without having to use the `global` keyword.

class MyGlobals1:

    glob1 = "x"
    glob2 = "y"
    main_class = None  # the main app class instance will be assigned to this

my_globals2 = [
    "value1",
    "value2"
]

my_globals3 = {
    "key1": "value1",
    "key2": "value2"
}

main.py

from base import *
from Cadfael import Monk


class MyApp:

    def __init__(self):

        # make this instance of MyApp globally available by assigning it to
        # an instance variable of `MyGlobals1`, which is imported from `base.py`
        MyGlobals1.main_class = self

        # set up a light source
        p_light = PointLight("point_light")
        p_light.set_color((1., 1., 1., 1.))
        # `camera` is imported from `base.py`...
        self.light = camera.attach_new_node(p_light)
        self.light.set_pos(5., -100., 7.)
        # ...and so is `render`
        render.set_light(self.light)

        # initialize a counter that will be incremented every frame
        self.counter = 0

        # start a task that increments the counter
        # `task_mgr` is imported from `base.py` as well...
        task_mgr.add(self.do_task, "do_task")

        # enable printing out the current value of the counter
        # ...as is `showbase`
        showbase.accept("space", self.print_counter)
        
        # Now, let's add a variable to our class--something
        # that we may want to access later...
        self.prayer = "Let's pray this works!"

        monk = Monk()

    def print_counter(self):

        print("Counter:", self.counter)

    def do_task(self, task):

        self.counter += 1

        return task.cont


app = MyApp()
showbase.run()

Cadfael.py (instead of catfile.py–get it? :stuck_out_tongue: )

from base import *


class Monk:

    def __init__(self):

        # "loader" should be available here, being imported from `base.py`
#        self.model = loader.load_model("smiley")
        # same for "load_model", which is even simpler:
        self.model = loader.load_model("smiley")
        # "render" should likewise be available here,
        # being imported from `base.py` also
        self.model.reparent_to(render)

        # enable printing out our prayers...
        showbase.accept("enter", self.pray)  # guess where `showbase` comes from

    def pray(self):

        # And now, let's say that we want to get that new
        # variable that we added to our main class.
        # Since we initialised an instance of our main class,
        # and since we added it to `MyGlobals1` in `base.py`,
        # that instance should be the thing referenced by
        # `MyGlobals1.main_class`. As a result, we
        # can access our main game class in this way!
        print(MyGlobals1.main_class.prayer)

This requires only a little bit more work, but it’s more future-proof, as the practice of abusing Python’s builtins might get phased out at some point.

Note that I don’t derive the main class from ShowBase–this is actually also a bit more clean (e.g. no danger of overwriting an attribute of ShowBase).

1 Like

Interesting–and funnily enough, I do that for pretty much everything but Panda’s global variables.

And if the use of such globals is intended to be phased out, then I’m inclined to second Epihaius’ suggestion of using a “common module”.

I’ll admit that I like having my “main class” be a sub-class of ShowBase–it makes sense to me that the “main class” be also the class that holds the driving game-loop.

(Plus I think that I recall seeing an engine-dev–rdb, perhaps?–advocating for sub-classing ShowBase. However, that was admittedly as opposed to the old approach of using DirectStart, if I recall correctly.)

Still, I don’t think that the question of whether to sub-class ShowBase or keep a separate instance of it is a major one; I have no major arguments against either way!

1 Like

Thank you as well. A separate config file for all the variables to share and hyper-parameter(player speed, scale, etc.) seems pretty nice to me. I’ll see which one is more convenient along the way.

Your code works amazing(and shockingly) well without too much effort. Only needed to change the key to accept from “+”, “-” to “a”, “b” and add some textures. I got the chunks and stuff working pretty well. Also managed to do multiple textures by only tweaking the algorithm a little bit.

Question: is adding blocks with more complex geometry a concern? Also how do I add blocks that is shorter than 1?

2 Likes