Soft Shadows

Hey all,

I just found some soft shadow class I wrote a long time ago. I originally wrote it for somebody else but I kept forgetting to also post it here at the forums.
It actually does not provide real soft shadows – it fakes them by hard rendering shadows and stuffing that into a buffer which has a shader applied to it that averages each pixel with its surrounding pixels.
It is basically just one class, ShadowManager, it is well documented (pydoc strings), clean and polished off. It is quite easy to use, look at the example.py for an example.
I hope someone finds it useful. Any questions, hints, improvements, bugs or constructive criticism is/are welcome.

Download link:
bin.pro-rsoft.com/softshadow.zip

Looks great!
Is it possible to modify it to work with per-pixel lighting? If yes, I will use for my game :slight_smile:
If you allow, of course.
EDIT: You are true treasury of programming wisdom, pro-rsoft! :smiley:

This shader already implements per-pixel lighting. Though, it is not hooked to Panda3D’s shader generator in any way.
Also, multiple lights or colored lights are not implemented yet, but thats an easy fix.
Sure, you can use this in your own game, feel free to modify this as well. It’s licensed under zlib.

I actually meant “is it possible to use it with shader generator?” I use it for hdr, bloom and normal maps, and will not drop all these things.
Does such modification require editing the shader? Since I don’t know Cg at all, I won’t be able to change the shader :frowning:
But I know Python a little, and want to learn. Could you, please, give a few hints how to change the python code, let’s say, where to start (if this is enough to make it work with shader generator)?

EDIT: Shader generator is very nice, but having no shadows today is very disappointing :frowning:

Sorry. If you want normal maps, you should add support for that yourself in the softshadow.sha file. The moment you do setShaderAuto on a node, the shadows wont show up on that object anymore.

If you really want to have both normal mapping and shadows, I suggest first having the shader generator generate a shader for it, retrieve that shader, and merge it with the softshadow.sha file. Though, I don’t recommend this if you don’t feel like digging in Cg shader code.

Wow, great job! Your code is so straight-forward and easy to use that it’s making me think of switching over to Panda3D for my next project. (I really need shadows!).

Nice work and thanks.

My noobiness may be showing, but does your code support material colors? I exported a simple cube egg from Blender with differently colored faces, but it just appears white when I use your soft shadows, but fine when I don’t.

Right, sorry. It wouldn’t take long to implement. As soon as I find some time, I’ll fix it.

That would be great. Thanks!

pro-rsoft, sorry for the question, of course, I could figure that out myself. But it would take quite some time, since I am just learning :blush:
With shader generator off, can I use your shadows together with normal mapping shader from Panda’s samples? I mean without changing their Cg codes, by combining them somehow in Python?

pro you get an A+!

Very nice example work even on my machine while the original had problems.

Do you have any suggestions of apply this to a 3d space … ie make every thing have a shadow. I am interested of casting shadow onto other ships mostly.

birukoff, you could combine those two shaders in python. Just that is extremely complicated and would take advanced knowledge of buffers. I recommend just merging the shaders. You need just 0.1% shader knowledge to merge those two shaders. I can help you if you run into any trouble.
EDIT: Oh, are you using the Shader Generator to manage normal mapping or are you using NodePath.setNormalMap (this way requires no shaders)?

treeform, thanks! 8)
If you look at example.py, you see you can init the ShadowManager with a parameter – which is the scene you apply it to. Like:

sm = ShadowManager(render)

for the entire scene. Though, I’m afraid it wrecks your current shaders. To make this compatible with all other shaders, either the ShadowManager needs to be modified or it needs to be integrated in a shader generator.

Another problem you might run into, treeform, is that in a large space scene, you might need a bigger buffer, but if you make the buffer larger, the performance drastically gets worse.
A solution might be to set up multiple ShadowManagers with relatively small buffers, one for each ship.
Though, I didn’t try this on a space-like scene yet – so I have no clue what will work best.

well i was thinking of an orthographical camera that comes form the sun (the main light source).

I can figure out the bounding volume of the ships i am looking at … some thing close by too.

Then cast the shadow at the center of that.

I don’t think this idea would be useful for everything not only space scenes but like general outside where you cast from the sun and care about shadowing only stuff that is close by.

Yeah i would merge your shaders with mine… cant be that hard?

I just got some time to try this.
What I don’t like about blurring the depthmap :

  1. far and small objects tend to be flushed out of depthmap, so they don’t cast shadows (see if you can notice boxes’ shadows).
  2. no shadow weight consistency, thanks to the blur. Shadow fades away at wrong spots, since you don’t control uniform blur, especially at close occluder-receiver spot (e.g. panda’s legs shadow).

    It’s only good for big and close objects, and approximately perpendicular to the light direction
    The modified pro-rsoft’s scene is here:
    ynjh.panda3dprojects.com/shadowm … ple-mod.py

I’ve improved the shadow sample, plus added standard percentage closer filtering (I believe it’s the simplest filter my pathetic GPU can deliver).
The improved parts are :

  1. smooth shading. Josh simply cut off the dot product below 0, so shadowed curvy surfaces look so dull. I can’t see any surface curvature. I scaled the original result -1~1 to ambient~1.
  2. Josh also applied shadow to facing-away-from-light surfaces. I can’t get good (1) if I let this happen. So, I render the shadow only for facing-toward-light surfaces. But the resulting blend is a little incorrect. Any help on this would be great.

Shots of original sample :

Shots of the improvements :

8 samples PCF result :

If you’re not happy with 8 samples and your GPU still has alot of power, use more samples.

Download:
ynjh.panda3dprojects.com/shadowm … _shadow.py
ynjh.panda3dprojects.com/shadowm … Shadow.sha

Thanks for your improvements. Unfortunately, I’m very busy with various other Panda3D-related things as well, so I don’t have anytime soon any time to add your improvements to my ShadowManager. But the ones who want better shadows can use the sample provided by ynjh_jo* instead.

  • I finally spelled it right at one try! :slight_smile:

Was it with or without looking at your keyboard ? Maybe it’s time to upgrade your brain-to-silicon wiring. :open_mouth:

Hi ynjh_jo!
I wanted to use your shader in my game, but encoutered a bug (this is my_new_shadow.py file without changes):

It’s not a bug.
Have you noticed this note in motion trails sample ?

# Panda does its best to hide the differences between DirectX and
# OpenGL.  But there are a few differences that it cannot hide.
# One such difference is that when OpenGL copies from a
# visible window to a texture, it gets it right-side-up.  When
# DirectX does it, it gets it upside-down.  There is nothing panda
# can do to compensate except to expose a flag and let the 
# application programmer deal with it.  You should only do this
# in the rare event that you're copying from a visible window
# to a texture.

if (base.win.getGsg().getCopyTextureInverted()):
    print "Copy texture is inverted."

LBuffer.setInverted(1) doesn’t make any difference here. You can go with config var :

loadPrcFileData('','copy-texture-inverted 1')

I see… Thank you!
Also, you included this:

if (base.win.getGsg().getSupportsShadowFilter()):
    mci.setShader(loader.loadShader('myShadow.sha'))
else:
    mci.setShader(loader.loadShader('shadow-nosupport.sha'))
base.cam.node().setInitialState(mci.getState())

First, you didn’t include shadow-nosupport.sha, second, on my laptop this check fails, while you shader still works excellent.

I have re-arranged the sample in the manner of ShadowManager by pro-rsoft. But something is wrong. This is without ShadowManager enabled:

And this - with:

What the hell is wrong? I look at the code, and don’t understand.

from pandac.PandaModules import GraphicsOutput, Texture, NodePath
from pandac.PandaModules import WindowProperties, GraphicsPipe
from pandac.PandaModules import FrameBufferProperties

class ShadowManager():
    def __init__(self, scene = render,
                bufferSize = 512, sampleGap = 0.12,
                lightIntensity = 1, ambient = 0.2, pushBias = 0.2,
                fov = 75, near = 1, far = 500):
        
        self.scene = scene
        self.bufXSize = self.bufYSize = bufferSize
        self.sampleGap = sampleGap
        
        self.lightIntensity = lightIntensity
        self.ambient = ambient
        self.pushBias = pushBias
        
        self.LBuffer = self.createOffscreenBuffer()
        print "Buffer created."
        self.Ldepthmap = self.createDepthMap()
        self.LBuffer.addRenderTexture(self.Ldepthmap,
                                    GraphicsOutput.RTMBindOrCopy,
                                    GraphicsOutput.RTPDepth)
        
        self.lightCam = self.createLightCamera(self.LBuffer,
                                    fov = fov, near = near, far = far)
        print "lightCam created."
        self.lightCam.setPos(render, 20, -20, 100)
        self.lightCam.lookAt(render, 0, 0, 0)
        
        mci = NodePath("Main Camera Initializer")
        mci.setShader(loader.loadShader("shaders/shadow.sha"))
        print "Shader loaded."
        base.cam.node().setInitialState(mci.getState())
        
        # Setup shader.
        self.scene.setShaderInput("light", self.lightCam)
        self.scene.setShaderInput("Ldepthmap", self.Ldepthmap)
        self.scene.setShaderInput("ambient", self.ambient, 0, 0, 1.0)
        self.scene.setShaderInput("intensity", self.lightIntensity,
                                            self.lightIntensity,
                                            self.lightIntensity, 0)
        self.scene.setShaderInput("texDisable", 0, 0, 0, 0)
        self.scene.setShaderInput("mapXel", 1.0/self.bufXSize,
                                            1.0/self.bufYSize, 1, 1)
        self.scene.setShaderInput("gap", self.sampleGap,
                                            self.sampleGap, 1, 1)
        self.adjustPushBias(0)
        self.adjustAmbient(0)
        self.adjustLightInstensity(0)
        self.adjustSampleGap(0)
        print "Shader inputs set."
        
    def createOffscreenBuffer(self, size = 512, sort = -2):
        winprops = WindowProperties.size(size, size)
        props = FrameBufferProperties()
        props.setRgbColor(1)
        props.setAlphaBits(1)
        props.setDepthBits(1)
        buffer = base.graphicsEngine.makeOutput(
                        base.pipe, "offscreen buffer",
                        sort, props, winprops,
                        GraphicsPipe.BFRefuseWindow,
                        base.win.getGsg(), base.win)
        if buffer == None:
            print "Video driver cannot create an offscreen buffer."
        return buffer
        
    def createDepthMap(self):
        tex = Texture()
        tex.setWrapU(Texture.WMClamp)
        tex.setWrapV(Texture.WMClamp)
        tex.setMinfilter(Texture.FTShadow)
        tex.setMagfilter(Texture.FTShadow)
        return tex
        
    def createLightCamera(self, buffer, fov = 75, near = 1, far = 100):
        cam = base.makeCamera(buffer)
        cam.reparentTo(render)
        cam.setPos(0, 0, 0)
        cam.lookAt(0, 0, 0)
        cam.node().getLens().setFov(fov)
        cam.node().getLens().setNearFar(near, far)
        #cam.setShaderOff(True)
        return cam
        
    def adjustPushBias(self, inc):
        self.pushBias += inc
        self.scene.setShaderInput("push",
                                self.pushBias,
                                self.pushBias,
                                self.pushBias, 0)

    def adjustLightInstensity(self, inc):
        self.lightIntensity=clampScalar(0.0,1.0,self.lightIntensity+inc)
        self.scene.setShaderInput("intensity",
                                self.lightIntensity,
                                self.lightIntensity,
                                self.lightIntensity, 0)

    def adjustAmbient(self, inc):
        self.ambient=clampScalar(0.01,1.0,self.ambient+inc)
        self.scene.setShaderInput("ambient",
                                self.ambient, 0, 0, 1.0)

    def adjustSampleGap(self, inc):
        self.sampleGap=clampScalar(0,1.0,self.sampleGap+inc)
        self.scene.setShaderInput("gap",
                                self.sampleGap,
                                self.sampleGap, 1, 1)
        
    def isInverted(self):
        if base.win.getGsg().getCopyTextureInverted():
            print "Copy texture is inverted."
            return True
        else:
            return False