Screen space decals

Below is some code to project a decal / sticker onto geometry using a depth buffer.

This can be useful for bullet holes, cosmetics, or environment splatting.

It doesn’t include handling of lighting / normals, and I could imagine instancing could be used on the decal volumes for even more decals.

decals

The red cube is there to show the volume, just change the fragment shader to discard to not show it.

# Screen Space / Volume decals

# http://www.popekim.com/2012/10/siggraph-2012-screen-space-decals-in.html
# http://gamedev.stackexchange.com/questions/70435/screen-space-decals-converting-world-to-decal-space
# http://www.humus.name/index.php?page=3D&ID=83
# https://discourse.panda3d.org/t/need-help-with-texture-matrix-for-glsl-shadows/12717/3 - for mat methods


from panda3d.core import loadPrcFileData

loadPrcFileData('', 'sync-video 1')
loadPrcFileData('', 'show-frame-rate-meter 1')
loadPrcFileData('', 'win-size 512 512')
loadPrcFileData('', 'basic-shaders-only 0')
loadPrcFileData('', 'show-buffers 1')
loadPrcFileData('', 'multisamples 8' ) 
loadPrcFileData('', 'textures-power-2 0' )
loadPrcFileData('', 'framebuffer-multisamples 8')
loadPrcFileData('', 'depth-bits 32')


import sys
from direct.showbase.ShowBase import ShowBase

from direct.filter.FilterManager import FilterManager
from panda3d.core import *
from pandac.PandaModules import *
from direct.gui.DirectGui import OnscreenText
from panda3d.core import Vec2, Vec4


class World(ShowBase):

  def __init__(self):

    basic_vertex = """
    #version 130
    uniform mat4 p3d_ModelViewProjectionMatrix;
    in vec2 p3d_MultiTexCoord0;
    in vec4 p3d_Vertex;
    in mat4 modelmat;
    out vec2 texCoord;
    void main() {
        gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
        texCoord = p3d_MultiTexCoord0;
    }
    """

    basic_frag = """
    #version 130
    uniform sampler2D p3d_Texture0;
    uniform sampler2D tex;
    in vec2 texCoord;
    void main()
    {
      gl_FragColor = texture(p3d_Texture0, texCoord.xy);
    }
    """

    vertex = """
    #version 330
    uniform mat4 p3d_ModelViewProjectionMatrix;
    in vec2 p3d_MultiTexCoord0;
    in vec4 p3d_Vertex;
    out vec4 position;
    out vec2 texCoord;
    void main() {
        position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
        gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
        texCoord = p3d_MultiTexCoord0;
    }
    """

    fragment = """
    #version 330
    uniform sampler2D p3d_Texture0;
    in vec2 texCoord;
    uniform sampler2D depthmap;
    uniform sampler2D decalmap;
    uniform vec2 screensize;
    in vec4 position;
    uniform mat4 p3d_ModelMatrix;
    uniform mat4 p3d_ModelMatrixInverse;
    uniform mat4 p3d_ModelMatrixTranspose;
    uniform mat4 p3d_ProjectionMatrix;
    uniform mat4 p3d_ProjectionMatrixInverse;
    uniform mat4 p3d_ViewProjectionMatrix;
    uniform mat4 p3d_ViewProjectionMatrixInverse;
    uniform mat4 p3d_ModelViewMatrix;
    uniform mat4 p3d_ModelViewMatrixInverse;
    uniform mat4 p3d_ViewMatrix;
    uniform mat4 p3d_ViewMatrixInverse;
    uniform mat4 p3d_ModelViewProjectionMatrix;
    uniform mat4 p3d_ModelViewProjectionMatrixInverse;
    vec4 reconstruct_pos(float z, vec2 uv_f)
    {
        vec4 sPos = vec4((uv_f * 2.0) - 1.0, z * 2.0 - 1.0, 1.0);
        sPos = p3d_ProjectionMatrixInverse * sPos;
        return vec4(sPos / sPos.w);
    }
    void main()
    {
        // Calculate the screen-space coordinates of the fragment
        vec2 u_resolution = vec2(screensize.x, screensize.y);
        vec2 st = gl_FragCoord.xy / u_resolution.xy;
        st += vec2(0.5f / u_resolution.x, 0.5f / u_resolution.y); //half pixel offset
        float aspect_ratio = u_resolution.y / u_resolution.x;
        vec2 texture_uv = st * aspect_ratio;
        // Sample the depth buffer to get the depth value
        float depth = texture(depthmap, texture_uv).x;
        vec4 worldPosition = reconstruct_pos(depth, st);
        worldPosition.w = 1.0f;
        vec4 localPos = p3d_ModelViewMatrixInverse * worldPosition;
        float dist = (localPos.y);
        float dist2 = (localPos.x);
        if (dist > 0.0f && dist2 > 0.0f && dist2 < 1.0f && dist < 1.0f && localPos.z < 1.0f && localPos.z > 0.0f)
        {
            vec2 uv = vec2(localPos.x, localPos.y);
            vec4 diffuseColor = texture(decalmap, uv);
            gl_FragColor = diffuseColor;
        }
        else
            //Show a red background for debugging.
            gl_FragColor = vec4(1.0f, 0.0f, 0.0f, 0.5);
            //Discard normally.
            //discard;
    }
    """

    ShowBase.__init__(self)
    render.setShaderAuto()

    # This creates the on screen title
    self.title = OnscreenText(text="Panda3D: Screen Space Decals",
                              style=1, fg=(1,1,1,1),
                              pos=(0.57,0.90), scale = .05)

    # Set the background color
    base.setBackgroundColor(.66, .714, 1)

    # Hit ESC to exit
    self.accept("v", base.bufferViewer.toggleEnable)
    self.accept("escape",sys.exit)

    self.screenSize = (512, 512)

    winprops = WindowProperties(size=self.screenSize)
    props = FrameBufferProperties()
    props.setDepthBits(1)
    props.setRgbaBits(32, 0, 0, 0)

    # Sets up a depth buffer, take note of the "1", which is the sort
    # to be rendered before the main buffer.
    self.depthbuffer = base.graphicsEngine.makeOutput(
        base.pipe, "offscreen buffer", 1,
        props, winprops,
        GraphicsPipe.BFRefuseWindow,
        base.win.getGsg(), base.win)
    
    self.depthmap = Texture()
    self.depthmap.setFormat(Texture.FDepthComponent)
    self.depthbuffer.addRenderTexture(self.depthmap, GraphicsOutput.RTMCopyRam, GraphicsOutput.RTPDepth)
    lens = self.cam.node().getLens()
    self.depthCam = self.makeCamera(self.depthbuffer,
        lens=lens,
        scene=render)
    self.depthCam.reparentTo(self.cam)

    self.filterManager = FilterManager(base.win, base.cam, self.screenSize[0], self.screenSize[1])
    self.albedo = Texture()

    # Final output
    self.finalBuffer = Texture()
    
    shader = Shader.make(Shader.SLGLSL, basic_vertex, basic_frag)
    self.decalshader = Shader.make(Shader.SLGLSL, vertex, fragment)

    # Deferred shading quad
    self.finalQuad = self.filterManager.renderSceneInto(colortex = self.albedo)
    self.finalQuad.setShader(shader)
    self.finalQuad.setShaderInput("tex", self.albedo)

    # Set the sort to be just after the depth buffer.
    self.filterManager.buffers[0].setSort(2)

    # Create ground plane.
    cm = CardMaker('card')
    self.ground = render.attachNewNode(cm.generate())
    self.ground.setP(-90)
    self.ground.setSz(200)
    self.ground.setSx(200)
    self.ground.setPos(-100,-100,0)
    self.ground.flattenStrong()
    self.ground.reparentTo(render)
    tex = loader.loadTexture('maps/grid.rgb')
    ts = TextureStage('ts')
    self.ground.setTexture(ts,tex)
    self.ground.setTexScale(ts, 16, 16)

    # Setup some boxes to project onto.
    self.box2 = loader.loadModel('box')
    self.box2.reparentTo(render)
    self.box2.setScale(1)
    self.box2.setY(1)

    self.box3 = loader.loadModel('box')
    self.box3.reparentTo(render)
    self.box3.setX(1)

    self.decalVolumes = []

    # The texture we want to project.
    self.decalmap = loader.loadTexture('maps/smiley.rgb')

    # Create some decals
    self.decalVolumes.append(self.addDecalVolume((0.5,0.5,-.5), (0,0,0)))
    self.decalVolumes.append(self.addDecalVolume((0.8,0.5,0.6), (90,90,0)))
    self.decalVolumes.append(self.addDecalVolume((-0.2,0.8,0.8), (0,0,0)))

    # Light
    self.alight = AmbientLight('ambientLight')
    self.alight.setColor(Vec4(0.1, 0.1, 0.2, 1))
    alightNP = render.attachNewNode(self.alight)

    dlight = DirectionalLight('dlight')
    dlight.setColor(VBase4(0.8, 0.8, 0.8, 1))
    dlnp = render.attachNewNode(dlight)
    dlnp.setHpr(45, -60, 0)
    render.setLight(dlnp)

  def addDecalVolume(self, pos, hpr):

    decal = loader.loadModel('box')
    decal.reparentTo(render)
    decal.setTwoSided(False)
    # Remove from depth buffer.
    decal.setDepthWrite(0)
    #decal.setDepthTest(0)

    # Fixes some z fighting.
    decal.setAttrib(DepthOffsetAttrib.make(1))

    decal.setTransparency(TransparencyAttrib.MAlpha)
    decal.flattenStrong()
    decal.setScale(0.6)
    decal.setPos(pos)
    decal.setHpr(hpr)

    # Note: Flattening after transforms breaks the position of the decal.

    # Shader inputs
    decal.setShaderInput('depthmap', self.depthmap)
    decal.setShaderInput('decalmap', self.decalmap)

    decal.setShaderInput('screensize', self.screenSize)

    decal.setShader(self.decalshader)

    return decal


w = World()
base.run()
6 Likes

Thanks for the example, but there is one problem, this shader gives an error.

:display:gsg:glgsg(error): glBlitFramebuffer has generated an error (GL_INVALID_OPERATION)

My operating system is Windows 10, a Radeon RX 580 series graphics card.

If you have a similar error, you need to change this line.

loadPrcFileData('', 'depth-bits 24')