Reproducing infinite grid shader example

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()
2 Likes