So, here’s my ProceduralTube class. I use it to make trees and such. There’s a rope in Panda3d which is likely more performant. However, this one can do a variable radius at each join. Also, it’s useful when you have known, fixed, points.
You can run it as main. You need to replace the ‘wood.png’ at the bottom with your own path. You might need to scale the UV as well, a few lines below ‘wood.png’.
You pass ProceduralTube.makeTube() a list of points (pts
) and a list an equal number of radii (radii
), one for the radius at each point. Alternatively, you can pass a constant for a uniform radius along the whole tube. The radialSlices
parameter controls how many slices run from one end to the other e.g. 4 makes an extruded square, and 8 makes an extruded stop sign. Of course the opposite cuts are cross-section at each point passed in the first parameter. The tangent
parameter keeps each of the radial slices lined up from joint to joint. If you don’t pass this, it will be generated. The stitch
parameter makes each join merge with the previous joint. I don’t know why you wouldn’t want this on, but you can have gaps if you want gaps.
from panda3d.core import GeomVertexFormat, GeomVertexData
from panda3d.core import Geom, GeomTriangles, GeomVertexWriter, GeomVertexRewriter
from panda3d.core import LVector3, Quat, Mat3
class ProceduralTube(object):
triangleCount = 0
vertexCount = 0
@classmethod
def makeTube(cls, pts, radii, radialSlices=4, tangent=None, stitch=True):
"""
:param pts: Iter of 3D points the tube passes through
:param radii: Iter of radius at each point.
:param radialSlices: Slices along major axis.
:param tangent: Normal to major axis, if given.
:param stitch: Flag to stitch each segment together or not.
"""
# format = GeomVertexFormat.getV3n3cpt2()
format = GeomVertexFormat.getV3n3t2()
vdata = GeomVertexData('tube', format, Geom.UHDynamic)
vertex = GeomVertexWriter(vdata, 'vertex')
normalWrtr = GeomVertexWriter(vdata, 'normal')
texcoord = GeomVertexWriter(vdata, 'texcoord')
axis = pts[1] - pts[0]
axis.normalize()
if tangent is None: # the tangent keeps everything aligned through out!!!!!!!!!
# make a shallow attempt to create a tangent through the y axis
# we can't pick y directly (the tube isn't likely orthogonal), so try to cross off of x
if axis.y != 0.0 or 0.0 != axis.z:
tangent = LVector3.unitX()
elif axis.y != 0.0 or 0.0 != axis.x:
tangent = LVector3.unitZ()
else:
tangent = LVector3.unitY()
tangent = axis.cross(tangent)
if tangent.normalize() and tangent == LVector3.zero():
raise ValueError("Tangent cannot be zero vector.")
tube = Geom(vdata)
stitchLoop = []
triCount = vertexCount = 0
faceLoopCount = 0
rotMat = Mat3()
lastTotalLen = [0.0, ]*(radialSlices + 1)
lastAxis = LVector3(0.0)
for edgeLoop in range(0, len(pts) - 1):
if pts[edgeLoop + 1] == pts[edgeLoop] and not __debug__:
continue
elif pts[edgeLoop + 1] == pts[edgeLoop] and __debug__:
raise ValueError("Cannot have consecutive points with same value. {} & {}".format(pts[edgeLoop + 1],
pts[edgeLoop]))
else:
axis = pts[edgeLoop + 1] - pts[edgeLoop]
# print('axis {}'.format(axis))
segLength = axis.length() # root calculation
axis = axis / segLength # don't do a 2nd root
# reorient the tangent with each segment using the original tangent
# 1st intermediate vector (orthogonal to our target)
# the old tangent, the new and the current axis will be on the same plane
# but the new will be 90 from the current axis
intermediate = (axis).cross(tangent)
segTangent = intermediate.cross(axis)
segTangent.normalize() # this may not be necessary. (axis).cross(tangent) is two unit vectors\
# degrees per radial slice
rotationStep = 360.0 / radialSlices
lastTexRadial = 0.0 - (rotationStep / 360.0)
newStitchLoop = []
# print('start: {}'.format(pts[edgeLoop]))
# print('end: {}'.format(pts[edgeLoop + 1]))
for radialCut in range(0, radialSlices + 1):
texStart = lastTotalLen[radialCut]
texEnd = lastTotalLen[radialCut] + segLength
texRadial = (lastTexRadial + rotationStep / 360.0)
lastTexRadial = texRadial
lastTotalLen[radialCut] += segLength
rotMat.setRotateMat(radialCut * rotationStep % 360.0, axis)
normToSave = rotMat.xformVec(segTangent)
normToSave.normalize() # this will be the norm for both vertices in this radial slice
if not stitch:
try:
next0 = (rotMat.xformVec(segTangent) * radii[edgeLoop]) + pts[edgeLoop]
next1 = (rotMat.xformVec(segTangent) * radii[edgeLoop + 1]) + pts[edgeLoop + 1]
except TypeError:
next0 = (rotMat.xformVec(segTangent) * radii) + pts[edgeLoop]
next1 = (rotMat.xformVec(segTangent) * radii) + pts[edgeLoop + 1]
else:
try:
# if we're stitching loops, put the far edge in newStitchLoop
# and read from the old stitchLoop on the near edge
# except for the first loop
if edgeLoop == 0:
next0 = (rotMat.xformVec(segTangent) * radii[edgeLoop]) + pts[edgeLoop]
next1 = (rotMat.xformVec(segTangent) * radii[edgeLoop + 1]) + pts[edgeLoop + 1]
newStitchLoop.append(next1) # prepare for the 1st repeated edge
else:
next0 = stitchLoop[radialCut] # read from the last repeated edge
next1 = (rotMat.xformVec(segTangent) * radii[edgeLoop + 1]) + pts[edgeLoop + 1]
newStitchLoop.append(next1) # prepare for the next repeated edge
except TypeError:
if edgeLoop == 0:
next0 = (rotMat.xformVec(segTangent) * radii) + pts[edgeLoop]
next1 = (rotMat.xformVec(segTangent) * radii) + pts[edgeLoop + 1]
newStitchLoop.append(next1) # prepare for the 1st repeated edge
else:
next0 = stitchLoop[radialCut] # read from the last repeated edge
next1 = (rotMat.xformVec(segTangent) * radii) + pts[edgeLoop + 1]
newStitchLoop.append(next1) # prepare for the next repeated edge
# print('cut: {}'.format(radialCut))
# print('0: {}'.format(next0))
# print('1: {}'.format(next1))
vertex.addData3(next0.x, next0.y, next0.z)
vertex.addData3(next1.x, next1.y, next1.z)
normalWrtr.addData3(normToSave.x, normToSave.y, normToSave.z)
normalWrtr.addData3(normToSave.x, normToSave.y, normToSave.z)
# color.addData4f(0.5, 0.5, 0.5, 1.0)
# color.addData4f(0.5, 0.5, 0.5, 1.0)
texcoord.addData2f(texStart, texRadial)
texcoord.addData2f(texEnd, texRadial)
vertexCount += 2
faceLoopCount += 1
stitchLoop = newStitchLoop
# the first index of the face loop we just completed equals
# the number of verts - the number of verts in the face loop
offset = vertexCount - (radialSlices + 1) * 2
tris = GeomTriangles(Geom.UHDynamic)
for t in range(0, 2 * (radialSlices + 1) - 2): # 2 triangles per slice
idx = t + offset
if t % 2 == 0:
triple = (idx, idx + 2, idx + 1)
else:
triple = (idx, idx + 1, idx + 2)
# print(triple)
tris.addVertices(triple[0], triple[1], triple[2])
# tie in the first two vertices of the face loop with the last two
tris.addVertices(vertexCount - 2, offset, vertexCount - 1)
tris.addVertices(vertexCount - 1, offset, offset + 1)
triCount += 2
tris.closePrimitive()
tube.addPrimitive(tris)
# print('############## END MAIN LOOP #################')
cls.triangleCount = triCount
cls.vertexCount = vertexCount
return tube, tangent
if __name__ == '__main__':
from direct.showbase.ShowBase import ShowBase
from panda3d.core import PandaNode, Point4, Point3, TextureStage, Texture, GeomNode
class App(ShowBase): # TODO render to picture and run tests against the rendering i.e. compare pixel to pixel
def __init__(self):
ShowBase.__init__(self)
origin = PandaNode('Origin')
originNp = self.render.attachNewNode(origin)
originNp.setHpr(0.0)
dummy = PandaNode('Dummy')
dummyNp = originNp.attachNewNode(dummy)
currentX = 0.025
currentW = currentX * 0.20
currentStep = 1.0
golden = 1.61803398875
stepsPerRound = 8.0
goldenStep = golden / stepsPerRound
spiralCnt = 10.0
ptCount = spiralCnt * stepsPerRound
pList = []
p1 = Point4(currentX, 0.0, 0.0, currentW)
dummyNp.setPos(p1.x, p1.y, p1.z)
# generate points in a spiral
for i in range(0, int(ptCount)):
tmp = dummyNp.getPos(self.render)
pList.append(Point4(tmp.x, tmp.y, tmp.z, currentW))
originNp.setR(
((360.0/stepsPerRound) + originNp.getR()) % 360.0 )
dummyNp.setPos(currentX, 0.0, 0.0)
currentStep = ((currentStep + 1.0) % stepsPerRound) + 1.0
currentX = currentX + goldenStep * currentX
currentW = currentW + goldenStep * currentW
pts = [Point3(p.x, p.y, p.z) for p in pList]
rads = [p.w for p in pList]
# lines = LineSegs('lines')
# for p in pList:
# lines.drawTo(p.x, p.y - 3, p.z)
# self.render.attachNewNode(lines.create())
# self.disableMouse()
# self.camera.setPos(0, -10, 0)
# pts = (
# LVector3(0.0, 0.0, 0.0),
# LVector3(0.0, 0.0, 1.0),
# LVector3(2.0, 0.0, 2.0),
# )
# rads = (
# 1.0,
# 1.0,
# 1.0
# )
tube = ProceduralTube.makeTube(pts, rads, radialSlices=64, stitch=True)
snode = GeomNode('tube') # segments would go here
snode.addGeom(tube[0])
print('tube:\n\ttriangles: {}\n\tvertices: {}'.format(ProceduralTube.triangleCount, ProceduralTube.vertexCount))
# wp = WindowProperties()
# wp.setSize(1000, 800)
# self.win.requestProperties(wp)
# self.axis = loader.loadModel("zup-axis")
# self.axis.reparentTo(self.render)
# self.axis.setScale(1.0)
# self.axis.setPos(0.0)
np = self.render.attachNewNode(snode)
# np.hide()
# np.setRenderModeFilledWireframe(Point3(0.1, 1.0, 0.1))
texture = loader.loadTexture('wood.png')
texture.setWrapU(Texture.WM_repeat)
np.setTexture(texture)
np.setTexScale(TextureStage.getDefault(), 10.0, 1.0) # U, V and U = along major axis
base = App()
base.run()