Rubiks Cube in Panda

Hello,
I’m new to panda and i’m trying to create simple game with rubiks cube. I’ve already create small cubes from GeomVertexData like in procedural-cube tutorial in samples. Then i arrange 27 cubes so they looks like rubiks cube and now i’m struggling with wall rotation, so i find out in internet that best way to rotate model around point is to create dummy node

pivotNode = render.attachNewNode('node')

attach it to model

cube.wrtReparentTo(pivotNode)

and then rotate that node. So I’ve done that, and everything is working fine to the moment when i’m trying to rotate multiple rubiks walls e.g. front and then left as they have common cubes.

rotating code looks like that

        pivotNode = render.attachNewNode('node')
        for cube in getRight(cubes):
            cube.wrtReparentTo(pivotNode)
        pivotNode.hprInterval(0.25,L_R_rotation).start()

and here is my question what would be best option to rotate those walls as when i do:

  1. attach 9 cubes of front wall to dummy node
  2. rotate dummy node
  3. attach 9 cubes of left wall to new dummy node
  4. rotate second dummy node
    won’t work properly.

please advise.

In case you haven’t done so already, try putting the cube reparenting into a function, wrap it in a function interval and create a Sequence that contains all of the needed intervals, something like this:

pivotNodeFront = render.attachNewNode('pivot_front')
pivotNodeLeft = render.attachNewNode('pivot_left')

def reparentCubes(wall, pivotNode):
    for cube in getWall(wall, cubes):
        cube.wrtReparentTo(pivotNode)

seq = Sequence(
               Func(reparentCubes, "front", pivotNodeFront),
               LerpHprInterval(pivotNodeFront, 0.25, frontRotation),
               Func(reparentCubes, "left", pivotNodeLeft),
               LerpHprInterval(pivotNodeLeft, 0.25, leftRotation)
              )
seq.start()

I’ve not done it yet and I’ll try your idea tomorrow, but I have one concern when i’m looking at your code. After

LerpHprInterval(pivotNodeFront, 0.25, frontRotation)

all my front cubes will be rotated according to pivotNodeFront in the moment when I’ll reparent them, will the front rotation remain on all front cubes and additionally 3 of front cube will receive left rotation?
I’m asking because in my approach when i was reparenting cubes they were losing their rotation.

Well it seems there’s no way to rotate the pivot relative to itself using a LerpHprInterval (unless I’m missing something, as I hardly ever use intervals myself), so maybe that’s why it happened.
Anyway, it means a little bit more work needs to be done, apart from the reparenting. You could make the interval relative to yet another node, whose hpr you set to the pivot hpr just before the interval/sequence starts, but perhaps its easier to detach the pivot’s children, clear the pivot’s transform and then reparent those children. That’s how I did it in the working code sample below:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.interval.IntervalGlobal import LerpHprInterval, Func, Sequence


def createCube(parent, index, cubeMembership, walls):

    vertexFormat = GeomVertexFormat.getV3n3cp()
    vertexData = GeomVertexData("cube_data", vertexFormat, Geom.UHStatic)
    tris = GeomTriangles(Geom.UHStatic)

    posWriter = GeomVertexWriter(vertexData, "vertex")
    colWriter = GeomVertexWriter(vertexData, "color")
    normalWriter = GeomVertexWriter(vertexData, "normal")

    vertexCount = 0

    for direction in (-1, 1):

        for i in range(3):

            normal = VBase3()
            normal[i] = direction
            rgb = [0., 0., 0.]
            rgb[i] = 1.

            if direction == 1:
                rgb[i-1] = 1.

            r, g, b = rgb
            color = (r, g, b, 1.)

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

                pos = VBase3()
                pos[i] = direction
                pos[(i + direction) % 3] = a
                pos[(i + direction * 2) % 3] = b

                posWriter.addData3f(pos)
                colWriter.addData4f(color)
                normalWriter.addData3f(normal)

            vertexCount += 4

            tris.addVertices(vertexCount - 2, vertexCount - 3, vertexCount - 4)
            tris.addVertices(vertexCount - 4, vertexCount - 1, vertexCount - 2)

    geom = Geom(vertexData)
    geom.addPrimitive(tris)
    node = GeomNode("cube_node")
    node.addGeom(geom)
    cube = parent.attachNewNode(node)
    x = index % 9 // 3 - 1
    y = index // 9 - 1
    z = index % 9 % 3 - 1
    cube.setScale(.4)
    cube.setPos(x, y, z)
    membership = set() # the walls this cube belongs to
    cubeMembership[cube] = membership
    
    if x == -1:
        walls["left"].append(cube)
        membership.add("left")
    elif x == 1:
        walls["right"].append(cube)
        membership.add("right")
    if y == -1:
        walls["front"].append(cube)
        membership.add("front")
    elif y == 1:
        walls["back"].append(cube)
        membership.add("back")
    if z == -1:
        walls["bottom"].append(cube)
        membership.add("bottom")
    elif z == 1:
        walls["top"].append(cube)
        membership.add("top")

    return cube


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        walls = {}
        pivots = {}
        rotations = {}
        cubeMembership = {}
        wallIDs = ("front", "back", "left", "right", "bottom", "top")
        hprs = {}
        hprs["front"] = hprs["back"] = VBase3(0., 0., 90.)
        hprs["left"] = hprs["right"] = VBase3(0., 90., 0.)
        hprs["bottom"] = hprs["top"] = VBase3(90., 0., 0.)
        wallOrders = {}
        wallOrders["front"] = wallOrders["back"] = ["left", "top", "right", "bottom"]
        wallOrders["left"] = wallOrders["right"] = ["back", "top", "front", "bottom"]
        wallOrders["bottom"] = wallOrders["top"] = ["left", "front", "right", "back"]

        for wallID in wallIDs:
            walls[wallID] = []
            pivots[wallID] = self.render.attachNewNode('pivot_%s' % wallID)
            rotations[wallID] = {"hpr": hprs[wallID], "order": wallOrders[wallID]}

        for i in range(27):
            createCube(self.render, i, cubeMembership, walls)

        self.directionalLight = DirectionalLight('directionalLight')
        self.directionalLightNP = self.cam.attachNewNode(self.directionalLight)
        self.directionalLightNP.setHpr(-20., -20., 0.)
        self.render.setLight(self.directionalLightNP)
        self.cam.setPos(-7., -10., 4.)
        self.cam.lookAt(0., 0., 0.)

        def reparentCubes(wallID):
            pivot = pivots[wallID]
            children = pivot.getChildren()
            children.wrtReparentTo(self.render)
            pivot.clearTransform()
            children.wrtReparentTo(pivot)
            for cube in walls[wallID]:
                cube.wrtReparentTo(pivot)

        def updateCubeMembership(wallID, negRotation=False):
            wallOrder = rotations[wallID]["order"]
            if not negRotation:
                wallOrder = wallOrder[::-1]
            for cube in walls[wallID]:
                oldMembership = cubeMembership[cube]
                newMembership = set()
                cubeMembership[cube] = newMembership
                for oldWallID in oldMembership:
                    if oldWallID in wallOrder:
                        index = wallOrder.index(oldWallID)
                        newWallID = wallOrder[index-1]
                        newMembership.add(newWallID)
                    else:
                        newMembership.add(oldWallID)
                for oldWallID in oldMembership - newMembership:
                    walls[oldWallID].remove(cube)
                for newWallID in newMembership - oldMembership:
                    walls[newWallID].append(cube)

        self.seq = Sequence()

        def addInterval(wallID, negRotation=False):
            self.seq.append(Func(reparentCubes, wallID))
            rot = rotations[wallID]["hpr"]
            if negRotation:
                rot = rot * -1.
            self.seq.append(LerpHprInterval(pivots[wallID], 2.5, rot))
            self.seq.append(Func(updateCubeMembership, wallID, negRotation))
            print "Added " + ("negative " if negRotation else "") + wallID + " rotation."

        def acceptInput():
            # <F> adds a positive Front rotation
            self.accept("f", lambda: addInterval("front"))
            # <Shift+F> adds a negative Front rotation
            self.accept("shift-f", lambda: addInterval("front", True))
            # <B> adds a positive Back rotation
            self.accept("b", lambda: addInterval("back"))
            # <Shift+B> adds a negative Back rotation
            self.accept("shift-b", lambda: addInterval("back", True))
            # <L> adds a positive Left rotation
            self.accept("l", lambda: addInterval("left"))
            # <Shift+L> adds a negative Left rotation
            self.accept("shift-l", lambda: addInterval("left", True))
            # <R> adds a positive Right rotation
            self.accept("r", lambda: addInterval("right"))
            # <Shift+R> adds a negative Right rotation
            self.accept("shift-r", lambda: addInterval("right", True))
            # <O> adds a positive bOttom rotation
            self.accept("o", lambda: addInterval("bottom"))
            # <Shift+O> adds a negative bOttom rotation
            self.accept("shift-o", lambda: addInterval("bottom", True))
            # <T> adds a positive Top rotation
            self.accept("t", lambda: addInterval("top"))
            # <Shift+T> adds a negative Top rotation
            self.accept("shift-t", lambda: addInterval("top", True))
            # <Enter> starts the sequence
            self.accept("enter", startSequence)

        def ignoreInput():
            self.ignore("f")
            self.ignore("shift-f")
            self.ignore("b")
            self.ignore("shift-b")
            self.ignore("l")
            self.ignore("shift-l")
            self.ignore("r")
            self.ignore("shift-r")
            self.ignore("o")
            self.ignore("shift-o")
            self.ignore("t")
            self.ignore("shift-t")
            self.ignore("enter")

        def startSequence():
            # do not allow input while the sequence is playing...
            ignoreInput()
            # ...but accept input again once the sequence is finished
            self.seq.append(Func(acceptInput))
            self.seq.start()
            print "Sequence started."
            # create a new sequence, so no new intervals will be appended to the started one
            self.seq = Sequence()

        acceptInput()


app = MyApp()
app.run()

Just press any of the keys documented in the acceptInput function to queue up as many rotations as you like and then hit to play back the resulting sequence.

Even though you probably did things quite differently, I hope you will find the above code useful.

WOW! Awesome man! I’m really grateful that you have spend your time and energy to help me :slight_smile:)
Made my day :slight_smile:

@Epihaius, @Poxy, thank you so much, it is exactly what I am looking for.

I added the middle slice rotation: Equator, Center, Standing. Press the first letter of Up/Down/Left/Right/Front/Back/Equetor/Center/Standing and Enter for clockwise rotation, and press Shift+the first letter and Enter for anti clockwise rotation.

# Based on Epihaius's work.
# https://discourse.panda3d.org/viewtopic.php?f=8&t=19317&p=108866#p108866
# Revision: adding Equator, Center, Standing slices rotation.

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from direct.interval.IntervalGlobal import LerpHprInterval, Func, Sequence


def createCube(parent, x, y, z, position, cubeMembership, walls):

    vertexFormat = GeomVertexFormat.getV3n3cp()
    vertexData = GeomVertexData("cube_data", vertexFormat, Geom.UHStatic)
    tris = GeomTriangles(Geom.UHStatic)

    posWriter = GeomVertexWriter(vertexData, "vertex")
    colWriter = GeomVertexWriter(vertexData, "color")
    normalWriter = GeomVertexWriter(vertexData, "normal")

    vertexCount = 0

    for direction in (-1, 1):

        for i in range(3):

            normal = VBase3()
            normal[i] = direction
            rgb = [0., 0., 0.]
            rgb[i] = 1.

            if direction == 1:
                rgb[i-1] = 1.

            r, g, b = rgb
            color = (r, g, b, 1.)

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

                pos = VBase3()
                pos[i] = direction
                pos[(i + direction) % 3] = a
                pos[(i + direction * 2) % 3] = b

                posWriter.addData3f(pos)
                colWriter.addData4f(color)
                normalWriter.addData3f(normal)

            vertexCount += 4

            tris.addVertices(vertexCount - 2, vertexCount - 3, vertexCount - 4)
            tris.addVertices(vertexCount - 4, vertexCount - 1, vertexCount - 2)

    geom = Geom(vertexData)
    geom.addPrimitive(tris)
    node = GeomNode("cube_node")
    node.addGeom(geom)
    cube = parent.attachNewNode(node)
    cube.setScale(.4)
    cube.setPos(x, y, z)
    membership = set() # the walls this cube belongs to
    position[cube] = [x, y, z]
    cubeMembership[cube] = membership
    # In Panda3D, X axis straightly points to right.
    # Y axis goes inside perpendicular to the screen.
    # Z axis is pointing up.
    
    
    if x == 1:
        walls["right"].append(cube)
        membership.add("right")
    elif x == -1:
        walls["left"].append(cube)
        membership.add("left")
    elif x == 0:
        walls["center"].append(cube)
        membership.add("center")
        
    if y == 1:
        walls["back"].append(cube)
        membership.add("back")
    elif y == -1:
        walls["front"].append(cube)
        membership.add("front")
    elif y==0:
        walls["standing"].append(cube)
        membership.add("standing")
        
    if z == -1:
        walls["down"].append(cube)
        membership.add("down")
    elif z == 1:
        walls["up"].append(cube)
        membership.add("up")
    elif z==0:
        walls["equator"].append(cube)
        membership.add("equator")

    return cube


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        
        walls = {}
        pivots = {}
        rotations = {}
        position = {}
        cubeMembership = {}
        #Equator slice is the slice between up and down faces, center slice between left and right faces, standing slice the left one,
        wallIDs = ("front", "back", "left", "right", "down", "up", "equator", "center", "standing")
        hprs = {}
        # VBase(Z,X,Y) if spin around Z, VBase3(90., 0., 0.).
        # The degree is positive following the right hand rule.
        hprs["right"] = VBase3(0., -90., 0.)
        hprs["center"] = VBase3(0., -90., 0.) # The ratation direction of the standing slice follows the front face.
        hprs["left"] = VBase3(0., 90., 0.)
        hprs["back"] = VBase3(0., 0., -90.)
        hprs["front"] = VBase3(0., 0., 90.)
        hprs["standing"] = VBase3(0., 0., 90.)# The ratation direction of the center slice follows the right face.
        hprs["down"] = VBase3(90., 0., 0.)
        hprs["up"] = VBase3(-90., 0., 0.)
        hprs["equator"] = VBase3(-90., 0., 0.) # The ratation direction of the equator slice follows the up face.      
        wallRotate = {}
        wallNegRotate = {}
        # Each rotation is a matrix.
        # The positive front rotation and the negative back rotation have the same matrix.
        # The standing slice follows the rules of the front face.
        
        wallRotate["right"] = wallRotate["center"] = wallNegRotate["left"] = [[1, 0, 0], [0, 0, -1], [0, 1, 0]]
        wallRotate["left"] = wallNegRotate["right"] = wallNegRotate["center"] = [[1, 0, 0], [0, 0, 1], [0, -1, 0]]

        wallRotate["back"] = wallNegRotate["standing"] = wallNegRotate["front"] = [[0, 0, 1], [0, 1, 0], [-1, 0, 0]]
        wallRotate["front"] = wallRotate["standing"] = wallNegRotate["back"] = [[0, 0, -1], [0, 1, 0], [1, 0, 0]]
        
        wallRotate["up"] = wallRotate["equator"] = wallNegRotate["down"] = [[0, -1, 0], [1, 0, 0], [0, 0, 1]]
        wallRotate["down"] = wallNegRotate["equator"] = wallNegRotate["up"] = [[0, 1, 0], [-1, 0, 0], [0, 0, 1]]
        

        for wallID in wallIDs:
            walls[wallID] = []
            pivots[wallID] = self.render.attachNewNode('pivot_%s' % wallID)
            rotations[wallID] = {"hpr": hprs[wallID]}
        #print walls
        #print pivots
        #print rotations
        for x in (-1, 0, 1):
            for y in (-1, 0, 1):
                for z in (-1, 0, 1):
                    createCube(self.render, x, y, z, position, cubeMembership, walls)

        self.directionalLight = DirectionalLight('directionalLight')
        self.directionalLightNP = self.cam.attachNewNode(self.directionalLight)
        self.directionalLightNP.setHpr(20., -20., 0.) 
        self.render.setLight(self.directionalLightNP)
        self.cam.setPos(7., -10., 4.)
        self.cam.lookAt(0., 0., 0.)

        def reparentCubes(wallID):
            pivot = pivots[wallID]
            children = pivot.getChildren()
            children.wrtReparentTo(self.render)
            pivot.clearTransform()
            children.wrtReparentTo(pivot)
            for cube in walls[wallID]:
                cube.wrtReparentTo(pivot)

        def updateCubeMembership(wallID, negRotation=False):
            for cube in walls[wallID]:
                oldMembership = cubeMembership[cube]
                # print "oldMembership",oldMembership
                # print "old position", position[cube]
                newMembership = set()
                cubeMembership[cube] = newMembership
                
                # X cordinate
                newPos = 0
                if not negRotation:                    
                    for j in range(3):                        
                        newPos = newPos + int(position[cube][j]) * int(wallRotate[wallID][j][0])
                else:
                    for j in range(3):
                        newPos = newPos + int(position[cube][j]) * int(wallNegRotate[wallID][j][0])

                if newPos == 1:
                    newMembership.add("right")
                elif newPos == -1:
                    newMembership.add("left")
                elif newPos == 0:
                    newMembership.add("center")
                newPosX = newPos


                # Y cordinate
                newPos = 0
                if not negRotation:                    
                    for j in range(3):                        
                        newPos = newPos + int(position[cube][j]) * int(wallRotate[wallID][j][1])
                else:
                    for j in range(3):
                        newPos = newPos + int(position[cube][j]) * int(wallNegRotate[wallID][j][1])

                if newPos == 1:
                    newMembership.add("back")
                elif newPos == -1:
                    newMembership.add("front")
                elif newPos == 0:
                    newMembership.add("standing")
                newPosY = newPos

                
                # Z cordinate
                newPos = 0
                if not negRotation:                    
                    for j in range(3):
                        newPos = newPos + int(position[cube][j]) * int(wallRotate[wallID][j][2])
                else:
                    for j in range(3):
                        newPos = newPos + int(position[cube][j]) * int(wallNegRotate[wallID][j][2])

                if newPos == 1:
                    newMembership.add("up")
                elif newPos == -1:
                    newMembership.add("down")
                elif newPos == 0:
                    newMembership.add("equator")
                newPosZ=newPos
                
                position[cube] = [newPosX, newPosY, newPosZ]
                # print "newMembership",newMembership
                # print "new position:", position[cube]
                
                for oldWallID in oldMembership - newMembership:
                    walls[oldWallID].remove(cube)
                for newWallID in newMembership - oldMembership:
                    walls[newWallID].append(cube)
                
                
        self.seq = Sequence()

        def addInterval(wallID, negRotation=False):
            self.seq.append(Func(reparentCubes, wallID))
            rot = rotations[wallID]["hpr"]
            if negRotation:
                rot = rot * -1.
            #Revision: 1.0 is the speed of rotation, 2.5 is slower.
            self.seq.append(LerpHprInterval(pivots[wallID], 1.0, rot))
            self.seq.append(Func(updateCubeMembership, wallID, negRotation))
            print "Added " + ("negative " if negRotation else "") + wallID + " rotation."
 

        def acceptInput():# Revision: top-->up, bottom-->down. Reverse rotation: back,up,right 
            # <F> adds a positive Front rotation
            self.accept("f", lambda: addInterval("front"))
            # <Shift+F> adds a negative Front rotation
            self.accept("shift-f", lambda: addInterval("front", True))
            # <B> adds a positive Back rotation
            self.accept("b", lambda: addInterval("back"))
            # <Shift+B> adds a negative Back rotation
            self.accept("shift-b", lambda: addInterval("back", True))


            
            # <L> adds a positive Left rotation
            self.accept("l", lambda: addInterval("left"))
            # <Shift+L> adds a negative Left rotation
            self.accept("shift-l", lambda: addInterval("left", True))
            # <R> adds a positive Right rotation
            self.accept("r", lambda: addInterval("right"))
            # <Shift+R> adds a negative Right rotation
            self.accept("shift-r", lambda: addInterval("right", True))


            
            # <D> adds d positive Down rotation
            self.accept("d", lambda: addInterval("down"))
            # <Shift+D> adds a negative Down rotation
            self.accept("shift-d", lambda: addInterval("down", True))
            # <U> adds a positive Up rotation
            self.accept("u", lambda: addInterval("up"))
            # <Shift+U> adds a negative Up rotation
            self.accept("shift-u", lambda: addInterval("up", True))
            
            # Rivision: to rotate the center slice
            # <C> adds a positive Back rotation
            self.accept("c", lambda: addInterval("center"))
            # <Shift+C> adds a negative Back rotation
            self.accept("shift-c", lambda: addInterval("center", True))            
            # Rivision: to rotate the equator slice
            # <E> adds a positive Back rotation
            self.accept("e", lambda: addInterval("equator"))
            # <Shift+E> adds a negative Back rotation
            self.accept("shift-e", lambda: addInterval("equator", True))
            # Rivision: to rotate the standing slice
            # <S> adds a positive Back rotation
            self.accept("s", lambda: addInterval("standing"))
            # <Shift+S> adds a negative Back rotation
            self.accept("shift-s", lambda: addInterval("standing", True))
            
            # <Enter> starts the sequence
            self.accept("enter", startSequence)

        def ignoreInput():
            self.ignore("f")
            self.ignore("shift-f")
            self.ignore("b")
            self.ignore("shift-b")
            self.ignore("l")
            self.ignore("shift-l")
            self.ignore("r")
            self.ignore("shift-r")
            self.ignore("d")
            self.ignore("shift-d")
            self.ignore("u")
            self.ignore("shift-u")
            self.ignore("enter")

        def startSequence():
            # do not allow input while the sequence is playing...
            ignoreInput()
            # ...but accept input again once the sequence is finished
            self.seq.append(Func(acceptInput))
            self.seq.start()
            # print "Sequence started."
            # create a new sequence, so no new intervals will be appended to the started one
            self.seq = Sequence()

        acceptInput()


app = MyApp()
app.run()

But, is it possible to draw a rubik cube like this picture?

How to set the color brighter alike? In this case the mapping from hexadecimal RGB values to Panda3D color (r, g, b, a) is as below:

Yellow:#f8ff00–>0.97, 1, 0, 1.0

White:#ffffff–>1, 1, 1, 1

Orange: #ffc100–>1, 0.756, 0, 1.0

Blue: #063cff–>0.024, 0.235, 1, 1

Green: #00ff00–>0, 1, 0, 1

Red: #ff0000–>1, 0, 0, 1