Can I create a flat-shaded cube using only 8 vertices?


#1

Hi everyone, just started learning Panda3d,
and I need to create procedural cubes with specified dimensions. I figured that I’ll only need 8 vertices for the entire Geom, and I could do with just two primitives. One t strip for the front, right and back face, and one for the top, left and bottom face. like this

The vertices are generated by looping through each of the 4 bottom vertices, and then writing one with a z component of 0, and one with the specified length.

for vert in [[0, 0], [x_len, 0], [0, y_len], [x_len, y_len]]:
    for z in [0, z_len]:
        self.ver_w.addData3f(vert[0], vert[1], z)
        self.col_w.addData4f(col[0], col[1], col[2], col[3])

Normals were kind of an afterthought, I thought one per vertice should do the trick, probably pointing outwards from each of the adjacent edges at an 135 Angle.

Which as all of you probably know, results in a cube, shaded like a sphere or whatever… I should have known this from my blender work, but I only ever considered regular face vertices, and “winged” the vertex normals.

So I read up on vertex normals and discovered that the panda vertex normals are actually face normals, and that they should be pointing in the direction of the triangles face. Well I only have 8 of those…

I created my own array format, to see if it would even let me create 3 columns for normals, and I also made 3 new normal writers, and using a really crude, ugly and quick prototype, I looped through 3 nested loops using the zip method, extracting the appropriate vector component for each normal writer and adding them all together. It’s shit, I know.

for a, b, c in zip(self.n_l1, self.n_l2, self.n_l3):
    self.nor1_w.addData3f(a[0], a[1], a[2])
    self.nor2_w.addData3f(b[0], b[1], b[2])
    self.nor3_w.addData3f(c[0], c[1], c[2])

n_l are the normal the lists containing all normals. If you want to, I can provide them

Only problem: It looks exactly the same, as if it had no normals at all, with or without a light, there’s no shading at all… Printing my vertex data however shows me this: pythonoutput . So the data is in there…

So my question: Should I give up on creating a rectangular prism / cube like it is in my code at the moment, and build it out of individual faces with their own vertices for a total of 4 * 8 = 36 vertices? Can I make my code work some other way?

Sorry for the long text… Thanks for taking your time :blush:
btw, the rest of the cube creation works fine, If you would like to see the code, I’ll post it, I documented it a lot, I just didn’t want to put out any more text…


#2

Hi, welcome to the forums!

The normals are vertex normals. So for flat shading, they should normally be the same for each vertex on the face, which means you need to duplicate the vertices for each face (so you end up with 6 * 4 = 24 vertices).

You can set the shade model to Geom.SM_flat_first_vertex (or last) using geom.setShadeModel which should make the driver take the normal for the face from the first vertex in that face, but I’ve never tried this myself.


#3

Thank you so much for your answer!!! Glad my wall of text did not immediately turn everyone off. However, when calling .setShadeModel on my cube Geom, it said

panda3d.core.Geom’ object has no attribute 'setShadeModel

I poked through the class index, and figured that I should maybe apply it on my primitive, to which I got:

cannot init abstract class

I’m really new to all of this, it’s probably just that I don’t understand how the framework works at all, but if you could explain to me on which object to call this, it would help a lot…

Also, your right, my calculations were way off, but 24 vertices for a simple cube still doesn’t feel right to me, especially because 16 of those would be duplicates, apart from their normal component of course…

And it would also mean I would probably have to make this code much longer. If you would know of a good source on how to build cubes procedurally, that would be great, because I really don’t like how they did it in the example application

Anyhow, thanks again for helping me out here, feels really nice to know there is someone out there to help (:


#5

Yeah, sorry, it’s a GeomPrimitive method. GeomTriangles and GeomTristrips inherit from GeomPrimitive, so you should call it on that object you are creating.

It’s not really as weird as you think to make a cube using 24 vertices, noting that the vertices get duplicated before being shaded on the GPU anyway. The only thing you save on is the few extra bytes you don’t have to transfer to the GPU.


#6

If you have a legitimate reason not to use a few more vertices, then you could actually pull it off if you’re willing to venture into shader territory. You can get a (approximate) flat normals using the cross product from the partial derivative from view position - sounds hard but it’s just:
vertex:
V=vec4(p3d_ModelViewMatrix *p3d_Vertex).xyz;
fragment:
vec3 normal = normalize( cross(dFdx(V), dFdy(V)) );

Here’s an article explaining it better then I can:
http://www.aclockworkberry.com/shader-derivative-functions/


#7

thanks again for your answer… However applying the setShadeModel method on my tstrip primitive gives me a funny error, which I can’t make any sense of:

AssertionError: compat != nullptr at line 373 of c:\buildslave\sdk-windows-i386\build\panda\src\gobj\geom.cxx

I guess I’ll just have to build it out of multiple faces and duplicate vertices. It just seemed a bit unnecessary / ugly to me at first, but to be fair I don’t know much about 3d.

It’s exciting to learn though, and panda seems like a great tool for what I plan on doing in the future.

this btw is the code I wrote to create cubes, in a class init:

# Generating all vertices based on given Params, by stepping through
# every vert on the base plane, instantiating it, and instantiating
# the same vertex with a z component again.
for vert in [[0, 0], [x_len, 0], [0, y_len], [x_len, y_len]]:
    for z in [0, z_len]:
        self.ver_w.addData3f(vert[0], vert[1], z)
        self.col_w.addData4f(col[0], col[1], col[2], col[3])

# creating a tstrips primitive, thats like a mantle around front,
# right and back faces.
self.c_yxy_prim = cor.GeomTristrips(geo.UHStatic)

# adding our vertices to the primitive
self.c_yxy_prim.addVertices(1, 0, 3, 2)
self.c_yxy_prim.addVertices(7, 6, 5, 4)
# closing our primitive
self.c_yxy_prim.closePrimitive()

# creating top, left and bottom mantle primitive, again using 1
# Tristrip object.
self.c_zxz_prim = cor.GeomTristrips(geo.UHStatic)
self.c_zxz_prim.addVertices(7, 3, 5, 1)
self.c_zxz_prim.addVertices(4, 0, 6, 2)
self.c_zxz_prim.closePrimitive()

Let’s see if I can come up with something sleek that works with normals.


#8

Yeah, that example could use some improvement I guess. (@rdb perhaps, if I have some time, I might volunteer to make it a bit more up-to-date, e.g. using snake_case method names, replacing addDataxf with add_datax (especially removing the “f” at the end, now that I know why this is important :wink: ), and perhaps even adding memoryview into the mix as an advanced alternative for GeomVertexWriter?)

So if, like me, you prefer a more “compact” creation code, the following might be to your liking (I’ve included procedural generation of normals, vertex colours and texture coordinates as well):

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase


def create_cube(parent):

    vertex_format = GeomVertexFormat.get_v3n3c4t2()
    vertex_data = GeomVertexData("cube_data", vertex_format, Geom.UH_static)
    tris_prim = GeomTriangles(Geom.UH_static)

    pos_writer = GeomVertexWriter(vertex_data, "vertex")
    normal_writer = GeomVertexWriter(vertex_data, "normal")
    color_writer = GeomVertexWriter(vertex_data, "color")
    uv_writer = GeomVertexWriter(vertex_data, "texcoord")

    vertex_count = 0
    # (left=purple, back=green, down=blue, right=red, front=yellow, up=white)
    colors = ((1., 0., 1.), (0., 1., 0.), (0., 0., 1.),
              (1., 0., 0.), (1., 1., 0.), (1., 1., 1.))

    for direction in (-1, 1):

        for i in range(3):

            normal = VBase3()
            normal[i] = direction
            r, g, b = colors[i if direction == -1 else i-3]
            color = (r, g, b, 1.)

            for a, b in ((-1., -1.), (-1., 1.), (1., 1.), (1., -1.)):

                pos = Point3()
                pos[i] = direction
                pos[(i + direction) % 3] = a
                pos[(i + direction * 2) % 3] = b
                uv = (max(0., a), max(0., b))

                pos_writer.add_data3(pos)
                normal_writer.add_data3(normal)
                color_writer.add_data4(color)
                uv_writer.add_data2(uv)

            vertex_count += 4

            tris_prim.add_vertices(vertex_count - 2, vertex_count - 3, vertex_count - 4)
            tris_prim.add_vertices(vertex_count - 4, vertex_count - 1, vertex_count - 2)

    geom = Geom(vertex_data)
    geom.add_primitive(tris_prim)
    node = GeomNode("cube_node")
    node.add_geom(geom)
    cube = parent.attach_new_node(node)

    return cube


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        # set up a light source
        p_light = PointLight("point_light")
        p_light.set_color((1., 1., 1., 1.))
        self.light = self.camera.attach_new_node(p_light)
        self.light.set_pos(5., -10., 7.)
        self.render.set_light(self.light)

        # create a cube parented to the scene root
        self.cube = create_cube(self.render)
#        tex = self.loader.load_texture("my_tex.png")
#        self.cube.set_texture(tex)


app = MyApp()
app.run()

#9

I’ll check that out later, thanks!


#10

Wow, I definitely prefer this one over the example. I guess I’ll have to think about this for a bit, before I implement it, I couldn’t have come up with something like that.
the pos_writer is really clever, and then simply this:

tris_prim.add_vertices(vertex_count - 2, vertex_count - 3, vertex_count - 4)
tris_prim.add_vertices(vertex_count - 4, vertex_count - 1, vertex_count - 2)

to make a face.

However, I don’t get how you add the tris prim outside of the loop, I thought a tri can only store 3 vertices, however it gets data added 6 times in the loop… And it’s all the same object, right?

What am I missing here?

Anyhow, thank you for this awesome answer, I really appreciate the effort!

Edit: You should definitely ask to get this into the samples.


#11

You can add as many triangles (i.e. triplets of vertices) to a GeomTriangles primitive as you like (each of these will become a kind of “subprimitive” within the GeomTriangles object; you can actually call methods like get_num_primitives on that object to see how many of these are stored there etc.). That’s likely the reason they’re called GeomTriangles (plural) instead of GeomTriangle, I suppose.

FWIW, there’s no need to call close_primitive on a simple primitive type like GeomTriangles, which are nowadays preferred over the complex primitive types such as GeomTristrips (these used to be more efficient I believe, but modern graphics cards have no problems rendering large GeomTriangles and such it seems, so it’s probably best to just use these simple types).


#12

Huh, TIL. Looking it up in the documentation it actually says that it can store any number of Triangles, I missed that. I suppose the individual triangles are generated by stepping by stepping through the vertex data that’s indexed by the tris_prim in pairs of three?


#13

Yup, that’s it. If you call get_vertices on the GeomTriangles, you will get a GeomVertexArrayData object, and when you print that, you can actually see what vertex indices are being used to define each triangle:

    array = tris_prim.get_vertices()
    print("array:", array)

Using a GeomVertexRewriter you could even reorder those indices to e.g. render a triangle inside-out, or use the same index for each vertex in a triangle to make it disappear. Cool stuff :slight_smile: .


#14

Yeah, that’s definitely a new paradigm for me to think in. When I first started out, my idea was to store the vertices in a dict, and then add them directly to the primitives. Then I learned that you call them via their index, and I realised that is was thinking way too complicated again :sweat_smile:.
I really appreciate this community here though.