Reproducing infinite grid shader example

Hi All,
I’m trying to reproduce an infinite grid shader example found here and am having difficulty. I’ve gotten up to the unprojection stage, but I’m having issues now where the full screen quad does not correctly clip at the z=0 plane and in fact does not seem to be rendering at all when I move the camera. I have a feeling that there is something I’m not understanding about what Panda3D is doing in the background or about the differences between OpenGL and Panda3D coordinate systems. I’ve tried adding the GeomNode with the shader in render2d as well and the result still does not look correct. Any help would be greatly appreciated! Code and shaders are below.

main.py

from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
    Shader,
    GeomNode,
    GeomVertexFormat,
    GeomVertexData,
    Geom,
    GeomVertexWriter,
    Vec3,
    GeomTriangles,
)

class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        # self.disableMouse()
        self._full_screen_quad()

    def _full_screen_quad(self):
        shader = Shader.load(Shader.SL_GLSL, vertex="grid.vert", fragment="grid.frag")

        vdata = GeomVertexData("square", GeomVertexFormat.getV3(), Geom.UHDynamic)

        vertex_writer = GeomVertexWriter(vdata, "vertex")

        # Define the vertices of the square
        vertices = [Vec3(-1, 0, -1), Vec3(1, 0, -1), Vec3(1, 0, 1), Vec3(-1, 0, 1)]

        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("square_node")
        square_node.addGeom(square_geom)

        # Create a NodePath to render the GeomNode
        square_np = self.render.attachNewNode(square_node)
        square_np.setShader(shader)

        return square_np

app = MyApp()
app.run()

grid.vert

#version 460 

// Uniform inputs
uniform mat4 p3d_ProjectionMatrix; // trans_apiview_to_apiclip
uniform mat4 p3d_ViewMatrix; // trans_world_to_apiview

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 viewCoords = inverse(p3d_ProjectionMatrix) * clipCoords;
    viewCoords = viewCoords.xyzw / viewCoords.w;

    vec4 worldCoords = inverse(p3d_ViewMatrix) * viewCoords;

    return  worldCoords.xyz;
}

// normal vertice projection
void main() {
    vec3 ogl_Vertex = vec3(p3d_Vertex.x, p3d_Vertex.z, 0);
    gl_Position = vec4(ogl_Vertex, 1.0);
    nearPoint = UnprojectPoint(p3d_Vertex.x, p3d_Vertex.z, 1.0);
    farPoint = UnprojectPoint(p3d_Vertex.x, p3d_Vertex.z, -1.0);
}

grid.frag

#version 460 

in vec3 nearPoint; // nearPoint calculated in vertex shader
in vec3 farPoint; // farPoint calculated in vertex shader

// Output to the screen
out vec4 p3d_FragColor;

void main() {
    float t = -nearPoint.z / (farPoint.z - nearPoint.z);
    p3d_FragColor = vec4(1.0, 0.0, 0.0, 1.0 * float(t > 0)); // opacity = 1 when t > 0, opacity = 0 otherwise
}
1 Like

Regarding the quad not rendering, that may be because you have it parented to “render”–i.e. the 3D scene root–and aren’t moving it that I see. As a result, it may well end up behind the camera, and thus out of rendering-view.

Now, I see that your shader appears to use its vertices in such a way that it should render on-screen–but that doesn’t help if the object itself has already been culled away due to being out of camera-view.

I’d suggest instead parenting it to “render2d”, which should keep it on-screen at all times.

(I might also suggest looking at the CardMaker class for a simpler means of creating such a quad–unless you specifically want to practice the procedural generation of geometry, of course.)

By the way, let me note that Panda provides access to the inverses of important matrices, I believe–see the following list, just beneath the list of standard matrices:
https://docs.panda3d.org/1.10/python/programming/shaders/list-of-glsl-inputs

Thanks for the advice. I did try switching over to render2d and called setTransparency(TransparencyAttrib.MAlpha) on the GeomNode. Now when I first run the program I get the lower half of the screen red and the upper half the default background color which I think is about what I would expect. However, there is no change when I fly the camera around using the default controls. Is there something that would prevent the shader from being run multiple times if the object is in render2d? Also I did consider using CardMaker and I probably will switch to that when I get this working. I just wanted to build the geometry directly so I could understand exactly what data was in the vertices and eliminate that as an issue. I’ve included updated code below.

main.py

from direct.showbase.ShowBase import ShowBase
from panda3d.core import (
    Shader,
    GeomNode,
    GeomVertexFormat,
    GeomVertexData,
    Geom,
    GeomVertexWriter,
    Vec3,
    GeomTriangles,
    TransparencyAttrib,
)

class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        # self.disableMouse()
        self._full_screen_quad()

    def _full_screen_quad(self):
        shader = Shader.load(Shader.SL_GLSL, vertex="grid.vert", fragment="grid.frag")

        vdata = GeomVertexData("square", GeomVertexFormat.getV3(), Geom.UHDynamic)

        vertex_writer = GeomVertexWriter(vdata, "vertex")

        # Define the vertices of the square
        vertices = [Vec3(-1, 0, -1), Vec3(1, 0, -1), Vec3(1, 0, 1), Vec3(-1, 0, 1)]

        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("square_node")
        square_node.addGeom(square_geom)

        # Create a NodePath to render the GeomNode
        square_np = self.render2d.attachNewNode(square_node)
        square_np.setTransparency(TransparencyAttrib.MAlpha)

        square_np.setShader(shader)

        return square_np

app = MyApp()
app.run()

grid.vert

#version 460 

// Uniform inputs
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 viewCoords = p3d_ProjectionMatrixInverse * clipCoords;
    viewCoords = viewCoords.xyzw / viewCoords.w;

    vec4 worldCoords = p3d_ViewMatrixInverse * viewCoords;

    return  worldCoords.xyz;
}

// normal vertice projection
void main() {
    vec3 ogl_Vertex = vec3(p3d_Vertex.x, p3d_Vertex.z, 0);
    gl_Position = vec4(ogl_Vertex, 1.0);
    nearPoint = UnprojectPoint(p3d_Vertex.x, p3d_Vertex.z, 1.0);
    farPoint = UnprojectPoint(p3d_Vertex.x, p3d_Vertex.z, -1.0);
}

Good progress, I think! :slight_smile:

Nope, not as far as I’m aware!

Well, looking at your shaders, you appear to be calculating points based on the vertices of the card–and specifically, based on the model-space (i.e. untransformed) coordinates of the card.

Which… don’t change. After all, those coordinates are just the coordinates of the vertices as specified when making the geometry.

I’ll confess that it’s not clear to me how this is supposed to produce a varying surface.

It seems to me that some form of transform should be called for, whether it’s drawn from the camera’s position and orientation, or the card is moved with the camera and its world-space coordinates used, or… what. But there should be something that corresponds with the position and orientation of the camera in some way in order to determine what the grid should look like from that position and orientation…

[edit]
Ah, no, I see now–the unprojection algorithm is incorporating the transformation. Fair enough!

[edit2]
Aaah, I believe that I’ve found the problem!

Okay, when I first looked at this, I didn’t really look closely at what it was doing. As a result, I didn’t realise that it was operating on the vertices of the quad, using the matrices applicable to the quad, in order to produce its data.

As a result, I made the mistake of recommending that you attach the quad to “render2d”–perhaps the simplest way to get a full-screen quad, true–but a way that results in the quad not moving, and having matrices that reflect that.

In this case, I suspect that the trick is to move the quad with the camera. In my testing I’ve chosen to do this by attaching it to neither “render” nor “render2d”, but instead to “self.cam”. (Note that this then may call for moving the quad forward a bit, in order to make sure that it’s not culled away.)

Further, I’m finding that the division by the “w”-coordinate in the unprojection algorithm seems to introduce a rendering issue; removing that line seems to fix it. I’m not sure of why that might be, however–perhaps some difference in the way that the quad is being handled in the Python code, or the origin of the matrices that they’re using.

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