Thank you! Reparenting to the camera was exactly what I needed to do. I took a few days to finish up adapting the implementation which I’ll provide down below in case anyone is interested (now in a single file with the shaders as strings). I also added the first person camera from here. The one thing I’m still trying to figure out is that I have to add 1.0 to the depth in computeDepth and computeLinearDepth functions for the fragment depth to be correct. I think this is coming from still not completely understanding the coordinate systems at play, but it seems to work so I’ll ignore it for now.
Happy New Year!
shader.py
from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
Shader,
GeomNode,
GeomVertexFormat,
GeomVertexData,
Geom,
GeomVertexWriter,
Vec3,
GeomTriangles,
TransparencyAttrib,
NodePath,
BitMask32,
WindowProperties,
CollisionNode,
CollisionSphere,
LMatrix4f,
)
from direct.showbase import DirectObject
#from pandac.PandaModules import WindowProperties
#from panda3d.core import CollisionHandlerPusher, CollisionNode, \
# CollisionSphere
from direct.task import Task
## First person camera controller, "free view"/"FPS" style.
#
# Simple camera mouse look and WASD key controller
# shift to go faster,
# r and f keys move camera up/down,
# q and e keys rotate camera,
# hit enter to start/stop controls.
# If a refNode is specified, heading and up/down are performed wrt the
# reference node (usually the root node of scene, i.e. base.render)
# and camera behaves more similarly to an "FPS" camera.
class FirstPersonCamera(DirectObject.DirectObject):
'''
First person camera controller.
'''
## Constructor
# @param gameaApp: the game application to which this controller
# applies, that should be ShowBase derived.
# @param camera: the camera to which this controller applies
# @param refNode: reference node wrt heading and up/down are performed
def __init__(self, gameApp, camera, refNode=None,
collisionHandler=None):
'''
Constructor
'''
self.gameApp = gameApp
self.camera = camera
if refNode != None:
self.refNode = refNode
else:
self.refNode = self.camera
self.running = False
self.time = 0
self.centX = int(self.gameApp.win.getProperties().getXSize() / 2)
self.centY = int(self.gameApp.win.getProperties().getYSize() / 2)
# key controls
self.forward = False
self.backward = False
self.fast = 1.0
self.left = False
self.right = False
self.up = False
self.down = False
self.up = False
self.down = False
self.rollLeft = False
self.rollRight = False
# sensitivity settings
self.movSens = 2
self.movSensFast = self.movSens * 5
self.rollSens = 50
self.sensX = self.sensY = 0.2
self.collisionHandler = collisionHandler
self.collideMask = BitMask32(0x10)
#press enter to get this camera controller
self.accept("enter", self.toggle)
## Get camera collide mask
def getCollideMask(self):
return self.collideMask
## Camera rotation task
def cameraTask(self, task):
dt = task.time - self.time
# handle mouse look
md = self.gameApp.win.getPointer(0)
x = md.getX()
y = md.getY()
if self.gameApp.win.movePointer(0, self.centX, self.centY):
self.camera.setH(self.refNode, self.camera.getH(self.refNode)
- (x - self.centX) * self.sensX)
self.camera.setP(self.camera, self.camera.getP(self.camera)
- (y - self.centY) * self.sensY)
# handle keys:
if self.forward == True:
self.camera.setY(self.camera, self.camera.getY(self.camera)
+ self.movSens * self.fast * dt)
if self.backward == True:
self.camera.setY(self.camera, self.camera.getY(self.camera)
- self.movSens * self.fast * dt)
if self.left == True:
self.camera.setX(self.camera, self.camera.getX(self.camera)
- self.movSens * self.fast * dt)
if self.right == True:
self.camera.setX(self.camera, self.camera.getX(self.camera)
+ self.movSens * self.fast * dt)
if self.up == True:
self.camera.setZ(self.refNode, self.camera.getZ(self.refNode)
+ self.movSens * self.fast * dt)
if self.down == True:
self.camera.setZ(self.refNode, self.camera.getZ(self.refNode)
- self.movSens * self.fast * dt)
if self.rollLeft == True:
self.camera.setR(self.camera, self.camera.getR(self.camera)
- self.rollSens * dt)
if self.rollRight == True:
self.camera.setR(self.camera, self.camera.getR(self.camera)
+ self.rollSens * dt)
self.time = task.time
return Task.cont
## Start to control the camera
def start(self):
self.gameApp.disableMouse()
self.camera.setP(self.refNode, 0)
self.camera.setR(self.refNode, 0)
# hide mouse cursor, comment these 3 lines to see the cursor
props = WindowProperties()
props.setCursorHidden(True)
self.gameApp.win.requestProperties(props)
# reset mouse to start position:
self.gameApp.win.movePointer(0, self.centX, self.centY)
self.gameApp.taskMgr.add(self.cameraTask, 'HxMouseLook::cameraTask')
#Task for changing direction/position
self.accept("w", setattr, [self, "forward", True])
self.accept("shift-w", setattr, [self, "forward", True])
self.accept("w-up", setattr, [self, "forward", False])
self.accept("s", setattr, [self, "backward", True])
self.accept("shift-s", setattr, [self, "backward", True])
self.accept("s-up", setattr, [self, "backward", False])
self.accept("a", setattr, [self, "left", True])
self.accept("shift-a", setattr, [self, "left", True])
self.accept("a-up", setattr, [self, "left", False])
self.accept("d", setattr, [self, "right", True])
self.accept("shift-d", setattr, [self, "right", True])
self.accept("d-up", setattr, [self, "right", False])
self.accept("r", setattr, [self, "up", True])
self.accept("shift-r", setattr, [self, "up", True])
self.accept("r-up", setattr, [self, "up", False])
self.accept("f", setattr, [self, "down", True])
self.accept("shift-f", setattr, [self, "down", True])
self.accept("f-up", setattr, [self, "down", False])
self.accept("q", setattr, [self, "rollLeft", True])
self.accept("q-up", setattr, [self, "rollLeft", False])
self.accept("e", setattr, [self, "rollRight", True])
self.accept("e-up", setattr, [self, "rollRight", False])
self.accept("shift", setattr, [self, "fast", 10.0])
self.accept("shift-up", setattr, [self, "fast", 1.0])
# setup collisions
# setup collisions
if self.collisionHandler != None:
#setup collisions
nearDist = self.camera.node().getLens().getNear()
# Create a collision node for this camera.
# and attach it to the camera.
self.collisionNP = self.camera.attachNewNode(CollisionNode("firstPersonCamera"))
# Attach a collision sphere solid to the collision node.
self.collisionNP.node().addSolid(CollisionSphere(0, 0, 0, nearDist * 1.1))
# self.collisionNP.show()
# setup camera "from" bit-mask
self.collisionNP.node().setFromCollideMask(self.collideMask)
# add to collisionHandler (Pusher)
self.collisionHandler.addCollider(self.collisionNP, self.camera)
#add camera to collision system
self.gameApp.cTrav.addCollider(self.collisionNP, self.collisionHandler)
## Stop to control the camera
def stop(self):
self.gameApp.taskMgr.remove("HxMouseLook::cameraTask")
mat = LMatrix4f(self.camera.getTransform(self.refNode).getMat())
mat.invertInPlace()
self.camera.setMat(LMatrix4f.identMat())
self.gameApp.mouseInterfaceNode.setMat(mat)
self.gameApp.enableMouse()
props = WindowProperties()
props.setCursorHidden(False)
self.gameApp.win.requestProperties(props)
self.forward = False
self.backward = False
self.left = False
self.right = False
self.up = False
self.down = False
self.rollLeft = False
self.ignore("w")
self.ignore("shift-w")
self.ignore("w-up")
self.ignore("s")
self.ignore("shift-s")
self.ignore("s-up")
self.ignore("a")
self.ignore("shift-a")
self.ignore("a-up")
self.ignore("d")
self.ignore("shift-d")
self.ignore("d-up")
self.ignore("r")
self.ignore("shift-r")
self.ignore("r-up")
self.ignore("f")
self.ignore("shift-f")
self.ignore("f-up")
self.ignore("q")
self.ignore("q-up")
self.ignore("e")
self.ignore("e-up")
self.ignore("shift")
self.ignore("shift-up")
# un-setup collisions
if self.collisionHandler != None:
# remove camera from the collision system
self.gameApp.cTrav.removeCollider(self.collisionNP)
# remove from collisionHandler (Pusher)
self.collisionHandler.removeCollider(self.collisionNP)
# remove the collision node
self.collisionNP.removeNode()
## Call to start/stop control system
def toggle(self):
if(self.running):
self.stop()
self.running = False
else:
self.start()
self.running = True
class MyApp(ShowBase):
ref_grid_vertex_shader = """
#version 460
uniform mat4 p3d_ProjectionMatrixInverse;
uniform mat4 p3d_ViewMatrixInverse;
in vec4 p3d_Vertex;
out vec3 nearPoint;
out vec3 farPoint;
vec3 UnprojectPoint(float x, float y, float z) {
vec4 clipCoords = vec4(x, y, z, 1.0);
vec4 worldCoords = p3d_ViewMatrixInverse * p3d_ProjectionMatrixInverse * clipCoords;
return worldCoords.xyz / worldCoords.w;
}
void main() {
gl_Position = vec4(p3d_Vertex.xyz, 1.0);
nearPoint = UnprojectPoint(p3d_Vertex.x, p3d_Vertex.y, 0.0);
farPoint = UnprojectPoint(p3d_Vertex.x, p3d_Vertex.y, 1.0);
}
"""
ref_grid_fragment_shader = """
#version 460
uniform mat4 p3d_ProjectionMatrix;
uniform mat4 p3d_ViewMatrix;
uniform float nearClip;
uniform float farClip;
uniform float majorScale;
uniform float minorScale;
in vec3 nearPoint;
in vec3 farPoint;
out vec4 p3d_FragColor;
vec4 grid(vec3 fragPos3D, float scale, bool drawAxis) {
vec2 coord = fragPos3D.xy * scale;
vec2 derivative = fwidth(coord);
vec2 grid = abs(fract(coord - 0.5) - 0.5) / derivative;
float line = min(grid.x, grid.y);
float minimumy = min(derivative.y, 1);
float minimumx = min(derivative.x, 1);
vec4 color = vec4(0.2, 0.2, 0.2, 1.0 - min(line, 1.0));
if(fragPos3D.x > -0.1 * minimumx && fragPos3D.x < 0.1 * minimumx)
color.z = 1.0;
if(fragPos3D.y > -0.1 * minimumy && fragPos3D.y < 0.1 * minimumy)
color.x = 1.0;
return color;
}
float computeDepth(vec3 pos) {
vec4 clip_space_pos = p3d_ProjectionMatrix * p3d_ViewMatrix * vec4(pos.xyz, 1.0);
return ((clip_space_pos.z + 1.0)/ clip_space_pos.w);
}
float computeLinearDepth(vec3 pos) {
vec4 clip_space_pos = p3d_ProjectionMatrix * p3d_ViewMatrix * vec4(pos.xyz, 1.0);
float clip_space_depth = (clip_space_pos.z + 1.0) / clip_space_pos.w;
return 1.0 - smoothstep(0.9, 1.0, clip_space_depth);
}
void main() {
float t = -nearPoint.z / (farPoint.z - nearPoint.z);
vec3 fragPos3D = nearPoint + t * (farPoint - nearPoint);
gl_FragDepth = computeDepth(fragPos3D);
float fade = computeLinearDepth(fragPos3D);
p3d_FragColor = (grid(fragPos3D, majorScale, true) + grid(fragPos3D, minorScale, true)) * float(t > 0);
p3d_FragColor.a *= fade;
}
"""
def __init__(self):
ShowBase.__init__(self)
self.disableMouse()
self._np_ref_grid = self._make_reference_grid()
self.panda = self.loader.loadModel("models/panda")
self.panda.setScale(0.1)
self.panda.setPos(0, 0, 0)
self.panda.reparentTo(self.render)
self.setBackgroundColor(0, 0, 0)
self.mouseLook = FirstPersonCamera(self, self.cam, self.render)
self.mouseLook.start()
self.accept("tab", self.mouseLook.start)
self.accept("escape", self.mouseLook.stop)
def _make_reference_grid(
self, major_unit: float = 1.0, minor_unit: float = 0.1
) -> NodePath:
shader = Shader.make(
Shader.SL_GLSL,
vertex=self.ref_grid_vertex_shader,
fragment=self.ref_grid_fragment_shader,
)
vdata = GeomVertexData("square", GeomVertexFormat.getV3(), Geom.UHDynamic)
vertex_writer = GeomVertexWriter(vdata, "vertex")
# Define the vertices of the square
vertices = [Vec3(-1, -1, 0), Vec3(1, -1, 0), Vec3(1, 1, 0), Vec3(-1, 1, 0)]
for vertex in vertices:
vertex_writer.addData3f(vertex)
# Create the square geometry
square_geom = Geom(vdata)
# Create a GeomTriangles object to define the square's triangles
square_triangles = GeomTriangles(Geom.UHDynamic)
# Define the triangles of the square
square_triangles.addVertices(0, 1, 2)
square_triangles.addVertices(2, 3, 0)
# Add the triangles to the square's geometry
square_geom.addPrimitive(square_triangles)
# Create a GeomNode to hold the square's geometry
square_node = GeomNode("reference_grid")
square_node.addGeom(square_geom)
# Create a NodePath to render the GeomNode
square_np = NodePath(square_node)
square_np.reparentTo(self.cam)
square_np.setPos(0, 0, 0)
square_np.setTransparency(TransparencyAttrib.MAlpha)
square_np.setShader(shader)
square_np.setShaderInput("nearClip", self.camLens.near)
square_np.setShaderInput("farClip", self.camLens.far)
square_np.setShaderInput("majorScale", 1 / major_unit)
square_np.setShaderInput("minorScale", 1 / minor_unit)
return square_np
app = MyApp()
app.run()