Problems Adding Texture To 2D GeomNode

I’ve been working through the Panda3D manual to implement “at-runtime” model generation, but am having issues mapping a basic texture to the GoemNode I created. Here’s the code I’m using:

from direct.showbase.ShowBase import ShowBase

from panda3d.core import Geom, GeomNode, GeomVertexArrayFormat, \
                         GeomVertexFormat, GeomVertexData, \
                         GeomVertexWriter, GeomTristrips, Texture, \
                         TextureStage, TexGenAttrib

from math import pi, sin, cos
from direct.task import Task

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

        # Disable default mouse controls
        self.disableMouse()
        
        # Initialize a single Vertex Array Format (VAF) with two columns
        vaf = GeomVertexArrayFormat()
        vaf.addColumn("vertex", 3, Geom.NT_uint8, Geom.C_point)
        vaf.addColumn("texcoord", 2, Geom.NT_uint8, Geom.C_texcoord)

        # Add VAF to new Vertex Format (VF) object
        vf = GeomVertexFormat()
        vf.addArray(vaf)

        # Register Vertex Format and overwrite back into "vf" 
        vf = GeomVertexFormat.registerFormat(vf)

        # Associate Vertex Format with a new Vertex Data (VD)
        vd = GeomVertexData('cubeface', vf, Geom.UH_static)

        # Set expected row count (number of vertices) in newVertex Data object
        vd.setNumRows(4)

        # Initialize writers for each column specified in the VAF
        vertex_writer = GeomVertexWriter(vd, "vertex")
        texture_writer = GeomVertexWriter(vd, "texcoord")

        # Simple vertex dictionary used to populate Vertex Data object
        verts = {
            0: {
                "vertex": (0,0,1),
                "uv": (0,1)
            },
            1: {
                "vertex": (0,0,0),
                "uv": (0,0)
            },
            2: {
                "vertex": (1,0,1),
                "uv": (1,0)
            },
            3: {
                "vertex": (1,0,0),
                "uv": (0,1)
            }
        }

        # Iterate through vertices and add to Vertex Data object
        for i in range(4):
            v = verts[i]
            vertex_writer.addData3(*v.get("vertex"))
            texture_writer.addData2(*v.get("uv"))

        # Initialize Tristrips primative and add vertices sequentially
        prim = GeomTristrips(Geom.UH_static)
        prim.add_consecutive_vertices(0, 4)
        prim.close_primitive()

        # Add primative to new Geom object
        geom = Geom(vd)
        geom.addPrimitive(prim)

        # Add Geom to new GeomNode
        gnode = GeomNode('gnode')
        gnode.addGeom(geom)

        # Attach GeomNode to scene graph at render Node Path
        cubeface_np = self.render.attachNewNode(gnode)

        # Load 16x16 pixel 2D texture to map onto cubeface
        tex = loader.loadTexture("./assets/models/maps/grassblock/side.png")
        cubeface_np.setTexture(tex)

        # Position camera to look at cube face (positioned at 0,0,0)
        self.camera.setPos(2,-5,0)
        self.camera.setH(20)


app = Game()
app.run()

Here’s a screenshot of the expected 16x16 pixel texture image: expected.png

And here’s the scene I get when I run the game: actual.png

I’d appreciate any input on what I’m doing wrong here. Thanks!

You are using unsigned 8bit integers for vertex location and texture coordinate… change Geom.NT_uint8 to Geom.NT_float32 and it should work as expected.

Also your texture coordinates aren’t correct and should look like this:

        verts = {
            0: {
                "vertex": (0,0,1),
                "uv": (0,1)
            },
            1: {
                "vertex": (0,0,0),
                "uv": (0,0)
            },
            2: {
                "vertex": (1,0,1),
                "uv": (1,1)
            },
            3: {
                "vertex": (1,0,0),
                "uv": (1,0)
            }
        }

Okay, I updated my component types to this:

vaf.addColumn("vertex", 3, Geom.NT_float32, Geom.C_point)
vaf.addColumn("texcoord", 2, Geom.NT_float32, Geom.C_texcoord)

I also updated the UV values as you mentioned. The texture works now, but instead of the 16x16 pixel image being placed directly on the object, it is transformed first making it look blurred. Here’s a screenshot:

Do you know how I can prevent this transformation without enlarging the original texture image?

I think that what you’re seeing is the result of the standard texture filtering.

Do you want your texture to retain the sharp edges between its original pixels? If so, you might change the filtering mode employed for that texture to be “nearest” (I think that it was). This should prevent that blurring, if I’m not much mistaken. Like so, when loading the texture:

tex = loader.loadTexture("./assets/models/maps/grassblock/side.png",
                         minfilter = SamplerState.FT_nearest,
                         magfilter = SamplerState.FT_nearest)

Note the two new parameters, which set the filters used for the purposes of mip-mapping.

1 Like

It’s also possible to see some texture bleeding at the edges, which can be resolved by setting the texture wrap mode to “clamp”, see:
https://docs.panda3d.org/1.10/python/programming/texturing/texture-wrap-modes#one-caution-about-a-common-wrap-mode-error

2 Likes

Great, that worked!

I was also wondering if there’s a way to programmatically assign the texture at the Geom level, and if there’s a performance difference when doing so?

You could assign a texture to a particular Geom by setting its RenderState from the containing GeomNode. That RenderState can be created using a RenderState.make_empty call, or the currently assigned state can be obtained by calling get_geom_state on the GeomNode. A TextureAttrib can then be created and added to that RenderState, which yields a new RenderState that can finally be assigned to the Geom by calling set_geom_state on the GeomNode:

geom_index = 0
old_state = geom_node.get_geom_state(geom_index)
tex_attr = TextureAttrib.make(texture_obj)
new_state = old_state.add_attrib(tex_attr)
geom_node.set_geom_state(geom_index, new_state)

Despite a few additional Python function calls, this shouldn’t noticeably affect performance.

However, you could also just pass a priority into the set_texture call on your NodePath if you simply want to make sure that the texture is applied at the Geom level (and assigned to ALL Geoms - or at least those with a lower priority for their texture attributes):

cubeface_np.set_texture(tex, 1)