Thanks for the feedback Bradamante. Great thread. I will look into LerpTex and friends. Any ideas on UvScrollNode ?

Here I have implemented my “Procedural manual transform” and The tunnel behaves usefully and looks to be calculating fast enough. So I guess I’ll go with that.

Next is this issue of animating the uvs (which I could also do manually within ‘turn’ but I want it as efficient as possible.

```
###
# do fullscreen etc first
from panda3d.core import loadPrcFileData
#loadPrcFileData('', 'fullscreen 1')
import math
import direct.directbase.DirectStart
from panda3d.core import Fog, Texture, GeomNode
from panda3d.core import TextNode
from panda3d.core import Light,Spotlight,AmbientLight,DirectionalLight
from panda3d.core import Vec3,Vec4,Point3, Mat4, VBase3
from panda3d.core import lookAt
from panda3d.core import GeomVertexFormat, GeomVertexData
from panda3d.core import Geom, GeomTriangles, GeomLines, GeomVertexWriter
from direct.gui.OnscreenText import OnscreenText
from direct.showbase.DirectObject import DirectObject
from direct.interval.MetaInterval import Sequence
from direct.interval.LerpInterval import LerpFunc
from direct.interval.FunctionInterval import Func
from direct.task.Task import Task
import sys
# tunnel defs [radius, length, ringsteps, lengthsteps, texturefile]
tunneldefs = [[12,60,16,8, "models/tunnel.jpg"],
[20,60,16,5, "models/wood.jpg"] ]
#A simple function to make sure a value is in a given range, -1 to 1 by default
def restrain(i, mn = -1, mx = 1): return min(max(i, mn), mx)
class World(DirectObject):
def __init__(self):
#base.disableMouse() #disable mouse control so that we can place the camera
base.oobe()
#base.oobeCull
#camera.setPosHpr(0,0,10, 0, -90, 0)
camera.setPosHpr(0,0,0, 0, 0 , 0)
# define for 35mm lenses
base.camLens.setFilmSize(24, 36)
base.camLens.setFocalLength(24)
# set the background color to black
base.setBackgroundColor(0,0,0)
# setup task to control tunnel
taskMgr.add(self.turn, "turn")
###World specific-code
#Define the keyboard input
#Escape closes the demo
self.accept('escape', sys.exit)
self.accept('1', self.inc_fov, [-2])
self.accept('2', self.inc_fov, [2])
#Load the tunel and start the tunnel
self.tunnels = [] # [[node, initialV, vdata], tunneldef]
for t in tunneldefs:
self.tunnels.append([self.init_tube(t[0], t[1], t[2], t[3], t[4]), t])
self.show_tunnels()
def show_tunnels(self):
i = 0
for t in self.tunnels:
print "tunnel", i
print " [0][0] Node=",t[0][0]
print " [0][1] initialV has", len(t[0][1]), "vertices"
print " [0][2] vdata has", repr(t[0][2])
print " [1] defs = [radius, length, ringsteps, tubesteps]]"
print " [1] defs =", t[1]
def myNormalize(self, myVec):
# can't normalise in-place
myVec.normalize()
return myVec
def circle(self, radius, steps):
""" Generate a single ring to be used for each ring in cylinder
"""
pos = []
norm = []
angle = math.pi*2/steps
cos = math.cos(angle)
sin = math.sin(angle)
xn, zn = radius, 0
for i in range(steps):
nf = 1/(xn*xn+zn*zn)**0.5
norm.append((xn*nf, zn*nf))
pos.append((xn,zn))
xt = xn
xn = cos*xn - sin*zn
zn = sin*xt + cos*zn
# do last one (duplicate of first to close)
norm.append(norm[0])
pos.append(pos[0])
return pos,norm
def init_tube(self, radius, length, ringsteps, tubesteps, texture="models/tunnel.jpg"):
#
length = length
tubesteps = tubesteps
ringsteps = ringsteps
initialV = []
form = GeomVertexFormat.getV3n3t2()#getV3n3cpt2()
vdata = GeomVertexData('cyl', form, Geom.UHDynamic)
vertex = GeomVertexWriter(vdata, 'vertex')
normal = GeomVertexWriter(vdata, 'normal')
#color = GeomVertexWriter(vdata, 'color')
texcoord = GeomVertexWriter(vdata, 'texcoord')
#
ring, normals = self.circle(radius, ringsteps)
ustep = 1.0/ringsteps
vstep = 1.0/(tubesteps-1)
ystep = float(length) / (tubesteps-1)
y = 0
v = 0
for i in range(tubesteps):
u=0
for j in range(ringsteps+1): # same as len(ring)
p = ring[j]
n = normals[j]
vertex.addData3f(p[0], y, p[1])
initialV.append(VBase3(p[0], y, p[1]))
normal.addData3f(self.myNormalize(Vec3(n[0],0,n[1])))
#color.addData4f(0.5, 1.0*j/ringsteps, 1.0*i/tubesteps, 1.0)
texcoord.addData2f(u, v)
u += ustep
#
y += ystep
v += vstep
# vdata now defined
print len(initialV), "Vertices in tunnel"
#print self.vdata
# make triangles
tris = []
for i in range(tubesteps-1):
for j in range(0,ringsteps):
idx = j+i*(ringsteps+1)
#print idx, [idx,idx+1,idx+ringsteps+1], [idx+ringsteps+1, idx+1, idx+1+ringsteps+1]
tris.append([idx,idx+1,idx+ringsteps+1]) # 1/2 quad
tris.append([idx+ringsteps+1, idx+1, idx+1+ringsteps+1]) # 2nd 1/2 quad
# have list make triangles into prims
#print "\n",tris
tubeprims = []
# make tube triangles
for t in tris:
prim = GeomTriangles(Geom.UHStatic)
prim.addVertices(t[0],t[1],t[2])
prim.closePrimitive()
tubeprims.append(prim)
# finalise
cylGeom = Geom(vdata)
for p in tubeprims:
cylGeom.addPrimitive(p)
#
tunnelA = GeomNode('TunnelA')
tunnelA.addGeom(cylGeom)
c = render.attachNewNode(tunnelA)
# texture
tex = loader.loadTexture(texture)
tex.setWrapU(1)
tex.setWrapV(1)
c.setTexture(tex)
return (c, initialV, vdata)
def turn(self, task):
""" Based on mouse input (later to be replaced)
bend tubes progressively into distance.
Would prefer a bspline here and to set each tube ring to position/orientation of bspline
"""
#Check to make sure the mouse is readable
if base.mouseWatcherNode.hasMouse():
mpos = base.mouseWatcherNode.getMouse()
#Here we multiply the values to get the amount of degrees to turn
screenYrot = restrain(mpos.getX()) * 150
screenXrot = restrain(mpos.getY()) * 200
Zaxis = Vec3( 0, 0, -1 ) # up vector
Xaxis = Vec3( 1, 0, 0 )
for tunnels in self.tunnels:
verts = GeomVertexWriter(tunnels[0][2], 'vertex')
tubesteps = tunnels[1][3]
length = tunnels[1][1]
# make up one mat per ring to move those ring's vertices
newmats = range(tubesteps)
pos = VBase3(0,0,0)
zangle = 0
xangle = 0
for i in range(tubesteps):
#print i,zangle, pos
scalefact = 1.1-float(i)/(tubesteps-1)
newmats[i] = Mat4.scaleMat(scalefact, 1, scalefact) * Mat4.rotateMat( xangle, Xaxis ) * Mat4.rotateMat( zangle, Zaxis )# * Mat4.translateMat( pos)
# calc next ring's values
#pos[1] += float(length)/(tubesteps-1) # ! using initial tube values
zangle += float(screenYrot)/(tubesteps -1)
xangle += float(screenXrot)/(tubesteps -1)
# Move the verts by writing new ones
i = 0
rstep = tunnels[1][2] + 1 # ringsteps
initialV = tunnels[0][1] # initial tube verts in VBase3 format
while not verts.isAtEnd():
ringidx = i/rstep
mat = newmats[ringidx]
#print i, ringidx
newv = mat.xformPoint(initialV[i])
#if i == 0: print " ",self.initialV[i],newv
verts.setData3f(newv)
i+=1
return task.cont #Task continues infinitely
def inc_fov(self, change):
base.camLens.setFocalLength(base.camLens.getFocalLength()+change)
print"focal = ", base.camLens.getFocalLength()+change
self.fxtask.start()
status = self.mySound.status()
self.musicBoxSound.play()
#End Class World
w = World()
run()
```