Combining the Auto Shader and Shadows

What would be the best way to combine autoshading with a shadow map?

OK, the best way would probably be to write your own ShaderGenerator, but what if you don’t want to mess around with that?

My first thought was to get the shader generated by the auto shader and then modify it to include the shadow map part, but that sounds like it would be tricky and a killer for performance.

Another option might be to render the shadows to a filter texture and then combine it with the image produced by the auto shader. I can’t think of anything wrong with this, and you could clean up the shadows a bit while you’re at it.

Is there some other way I haven’t thought of?

Here’s an idea. Modify your shadow shader that it just outputs grayscale lighting information. Render that into a fullscreen quad that you put in front of the view. Set the blend mode to multiply or subtract, and voila.

Well yeah, it worked. :slight_smile:

Still need to find the best way to combine the textures, but it looks pretty good. Only problem is that when I blur the shadows it bleeds into other objects. Oh well, the extra blurring isn’t really necessary anyway, I’m already using a PCEFilter on the shadow camera.

I’ll post the code and some pics if anyone’s interested.

/me is always interested in any working panda code :smiley:

True, real-time soft shadows are a pretty difficult task. There are good reasons they aren’t used in too many games (actually, I’d be a little surprised if any games used them).

Here’s a link to a paper I found that provides a survey of soft shadow methods, in case you’re interested, but for game purposes I think you’re best off sticking with PCF shadow maps.

artis.inrialpes.fr/Publications/2003/HLHS03a/

I guess it’s harder to find somebody NOT interested :smiley:

OK, well here’s the code I used for setting up the buffers and filters (using the createOffscreenBuffer method from Pro-softs soft shadow example). Not sure if this is the best way or not.

# Create the depth buffer plus a texture to store the output in
        depthbuffer = self.createOffscreenBuffer(-3)
        self.depthmap = Texture()
        depthbuffer.addRenderTexture(self.depthmap, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor)

        # Create the shadow buffer plus a texture to store the output in
        shadowbuffer = self.createOffscreenBuffer(-2)
        self.shadowmap = Texture()
        shadowbuffer.addRenderTexture(self.shadowmap, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor)

        manager = FilterManager(base.win, base.cam)  
        
        # First filter stage just blurs the shadow map
        # Just blurring in x-direction for now
        self.blurtex = Texture()
        blurquad = manager.renderQuadInto(colortex=self.blurtex)  
        blurquad.setShader(Shader.load("blurx.sha"))
        blurquad.setShaderInput("tex", self.shadowmap) 
        
        # Second filter renders the scene and blends in the shadow texture
        # note that by default the scene gets renders with autoShaders
        self.rendertex = Texture()
        renderquad = manager.renderSceneInto(colortex=self.rendertex)         
        renderquad.setShader(Shader.load("shadowcombiner.sha"))
        renderquad.setShaderInput("render", self.rendertex)
        renderquad.setShaderInput("shadow", self.blurtex)      

Now here’s the camera setup.

# Make the shadow caster camera
        self.castercam = base.makeCamera(depthbuffer)
        self.castercam.node().setScene(self.scene)
        self.castercam.node().getLens().setFov(fov)
        self.castercam.node().getLens().setNearFar(near, far)

        # Put a shader on the shadow caster camera.
        lci = NodePath(PandaNode("lightCameraInitializer"))
        lci.setShader(loader.loadShader("caster.sha"))
        self.castercam.node().setInitialState(lci.getState())

        # Make the shadow camera as a copy of base.cam
        self.shadowcam = base.makeCamera(shadowbuffer, lens=base.cam.node().getLens())
        self.shadowcam.reparentTo(base.cam)

        # Put a shader on the shadow camera.
        lci = NodePath(PandaNode("lightCameraInitializer"))
        lci.setShader(loader.loadShader("shadowonly.sha"))
        self.shadowcam.node().setInitialState(lci.getState())

And now for the shaders. blurx.sha and caster.sha are just copies of Pro-softs. Here’s shadowonly.sha

//Cg

void vshader(float4 vtx_position : POSITION,
             float2 vtx_texcoord0: TEXCOORD0,
             float3 vtx_normal: NORMAL,

             uniform float4x4 trans_model_to_clip_of_light,
             uniform float4x4 mat_modelproj,
             uniform float4 mspos_light,
             uniform float4 k_props,

             out float4 l_position : POSITION,
             out float4 l_shadowcoord : TEXCOORD0,
	     out float  l_smooth : TEXCOORD1,
	     out float4 l_lightclip : TEXCOORD2
             )

{
	float4 position = vtx_position;

	// vertex position
	l_position = mul(mat_modelproj, position);

	// Calculate the surface lighting factor.
	l_smooth = saturate(dot(vtx_normal, normalize(mspos_light - position)));

	// Calculate light-space clip position.
	float4 pushed = position + float4(vtx_normal*0.2, 0);
	l_lightclip = mul(trans_model_to_clip_of_light, pushed);

	// Calculate shadow-map texture coordinates.
	l_shadowcoord = l_lightclip * float4(0.5,0.5,0.5,1.0) + l_lightclip.w * float4(0.5,0.5,0.5,0.0);
}



float2 poissonDisk[16] = {
	float2( -0.94201624, -0.39906216 ),
	float2( 0.94558609, -0.76890725 ),
	float2( -0.094184101, -0.92938870 ),
	float2( 0.34495938, 0.29387760 ),
	float2( -0.91588581, 0.45771432 ),
	float2( -0.81544232, -0.87912464 ),
	float2( -0.38277543, 0.27676845 ),
	float2( 0.97484398, 0.75648379 ),
	float2( 0.44323325, -0.97511554 ),
	float2( 0.53742981, -0.47373420 ),
	float2( -0.26496911, -0.41893023 ),
	float2( 0.79197514, 0.19090188 ),
	float2( -0.24188840, 0.99706507 ),
	float2( -0.81409955, 0.91437590 ),
	float2( 0.19984126, 0.78641367 ),
	float2( 0.14383161, -0.14100790 )
};


float PCF_Filter( float2 uv, float z, float r, sampler2D map )
{
  float sum = 0.0f;
  for ( int i = 0; i < 8; ++i )
  {
    float2 offset = poissonDisk[i] * r;    
    float mapval = tex2D(map,uv+offset);
    float diff = z-mapval;
    sum += (diff<=0);
  }
  return saturate(sum / 8.0);
}


void fshader(in float4 l_shadowcoord : TEXCOORD0,
             in float  l_smooth : TEXCOORD1,
             in float4 l_lightclip : TEXCOORD2,
             uniform sampler2D k_depthmap : TEXUNIT0,
             out float4 o_color:COLOR)
{
  float3 circleoffs = float3(l_lightclip.xy / l_lightclip.w, 0);
  float falloff = saturate(1.0 - dot(circleoffs, circleoffs));
  float4 proj = l_shadowcoord / l_shadowcoord.w;
  
  float mapval = tex2D(k_depthmap,proj.xy);
  float diff = proj.z-mapval;
  // next line is a bit of a hack ATM
  float shade = (diff>0) ? PCF_Filter(proj.xy,proj.z,0.2*diff,k_depthmap) : 1;
  o_color = falloff * l_smooth * shade;
} 

and here’s shadowcombiner.sha

//Cg
//
//Cg profile arbvp1 arbfp1

void vshader(float4 vtx_position : POSITION, 
             float2 vtx_texcoord0 : TEXCOORD0,
             out float4 l_position : POSITION,
      	     out float2 l_texcoord0 : TEXCOORD0,
      	     uniform float4 texpad_render, 
             uniform float4x4 mat_modelproj)
{
  l_position=mul(mat_modelproj, vtx_position);

  l_texcoord0 = vtx_position.xz * texpad_render.xy + texpad_render.xy; 
}


void fshader(float2 l_texcoord0 : TEXCOORD0,
             out float4 o_color : COLOR,
             uniform sampler2D k_render : TEXUNIT0,
             uniform sampler2D k_shadow : TEXUNIT1)
{
	// this next line is a bit of a hack, but it looks OK
  	o_color  = saturate(tex2D(k_render, l_texcoord0) * (tex2D(k_shadow, l_texcoord0)+0.25));
}

I think I’ll change shadowonly.sha so that it renders all attributes for the shadow casting light, including textures. Then shadow combiner can just add the two textures instead of the hack that I’ve got there at the moment.

Images to follow…

Here’s an image of it running…

From left to right the buffers are: blurred shadowmap, Autoshader rendered scene, depthmap, raw shadow map with PCFFilter.

Awesome work! Finally we have shadows with shader generator! It’s a relief. Can you post complete example/tutorial how you did it?

Thanks.

I’ve made a few mods, but there are still a few issues. Unfortunately, I don’t have time to polish it up ATM.

I’ve tossed out the blur stage since it slows it down a lot, doesn’t add much, and introduces other problems.

The shadows are now rendered white by shadowonly2.sha, and shadowcombiner.sha, now just subtracts a fraction of the “light” colour.

Anyway here’s the shadowManager class, which is just a slight modification of pro-rsoft’s.

from pandac.PandaModules import GraphicsOutput, Texture, NodePath, Vec3
from pandac.PandaModules import PandaNode, WindowProperties, GraphicsPipe
from pandac.PandaModules import ShaderGenerator, Shader
from pandac.PandaModules import FrameBufferProperties, Vec4
from direct.filter.FilterManager import FilterManager

class ShadowManager():
    """This class manages shadows for a scene."""

    def __init__(self, scene = base.render, fov = 40, near = 10, far = 100):
        self.scene = scene

        # Create the depth buffer plus a texture to store the output in
        depthbuffer = self.createOffscreenBuffer(-3)
        self.depthmap = Texture()
        depthbuffer.addRenderTexture(self.depthmap, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor)

        # Create the shadow buffer plus a texture to store the output in
        shadowbuffer = self.createOffscreenBuffer(-2)
        self.shadowmap = Texture()
        shadowbuffer.addRenderTexture(self.shadowmap, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor)

        manager = FilterManager(base.win, base.cam)  
        
        # blend shadow map with render output
        self.rendertex = Texture()
        renderquad = manager.renderSceneInto(colortex=self.rendertex)         
        renderquad.setShader(Shader.load("shadowcombiner.sha"))
        renderquad.setShaderInput("render", self.rendertex)
        renderquad.setShaderInput("shadow", self.shadowmap) 
        renderquad.setShaderInput("light", 1,1,1,1)        
        
        # Make the shadow caster camera
        self.castercam = base.makeCamera(depthbuffer)
        self.castercam.node().setScene(self.scene)
        self.castercam.node().getLens().setFov(fov)
        self.castercam.node().getLens().setNearFar(near, far)

        # Put a shader on the shadow caster camera.
        lci = NodePath(PandaNode("lightCameraInitializer"))
        lci.setShader(loader.loadShader("caster.sha"))
        self.castercam.node().setInitialState(lci.getState())

        # Make the shadow camera
        self.shadowcam = base.makeCamera(shadowbuffer, lens=base.cam.node().getLens())

        # Put a shader on the shadow camera.
        lci = NodePath(PandaNode("lightCameraInitializer"))
        lci.setShader(loader.loadShader("shadowonly2.sha"))
        self.shadowcam.node().setInitialState(lci.getState())
        self.shadowcam.reparentTo(base.cam)

        # Set the shader inputs
        self.scene.setShaderInput("light", self.castercam)
        self.scene.setShaderInput("depthmap", self.depthmap)

    # These are two functions which help creating two different kind of offscreen buffers.
    def createOffscreenBuffer(self, sort):
        winprops = WindowProperties.size(1024,1024)
        props = FrameBufferProperties()
        props.setRgbColor(1)
        props.setAlphaBits(1)
        props.setDepthBits(1)
        return base.graphicsEngine.makeOutput(
            base.pipe, "offscreenBuffer",
            sort, props, winprops,
            GraphicsPipe.BFRefuseWindow,
            base.win.getGsg(), base.win)

Here’s shadowonly2.sha:

//Cg

void vshader(	
		float4 vtx_position 		: POSITION,
		float3 vtx_normal			: NORMAL,
		uniform float4x4 trans_model_to_clip_of_light,
		uniform float4x4 mat_modelproj,
		uniform float4 mspos_light,

		out float4 l_back,
		out float4 l_position : POSITION,
		out float4 l_shadowcoord 	: TEXCOORD0,
		out float4 l_lightclip 		: TEXCOORD1
	)
{
	float4 position = vtx_position;
	
	// vertex position
	l_position = mul(mat_modelproj, position);
	
	// backface?
	l_back = dot(vtx_normal, normalize(mspos_light - position));

	// Calculate light-space clip position.
	float4 pushed = position + float4(vtx_normal*0.2, 0);
	l_lightclip = mul(trans_model_to_clip_of_light, pushed);

	// Calculate shadow-map texture coordinates.
	l_shadowcoord = l_lightclip * float4(0.5,0.5,0.5,1.0) + l_lightclip.w * float4(0.5,0.5,0.5,0.0);
}

float2 poissonDisk[16] = {
	float2( -0.94201624, -0.39906216 ),
	float2( 0.94558609, -0.76890725 ),
	float2( -0.094184101, -0.92938870 ),
	float2( 0.34495938, 0.29387760 ),
	float2( -0.91588581, 0.45771432 ),
	float2( -0.81544232, -0.87912464 ),
	float2( -0.38277543, 0.27676845 ),
	float2( 0.97484398, 0.75648379 ),
	float2( 0.44323325, -0.97511554 ),
	float2( 0.53742981, -0.47373420 ),
	float2( -0.26496911, -0.41893023 ),
	float2( 0.79197514, 0.19090188 ),
	float2( -0.24188840, 0.99706507 ),
	float2( -0.81409955, 0.91437590 ),
	float2( 0.19984126, 0.78641367 ),
	float2( 0.14383161, -0.14100790 )
};

float PCF_Filter( float2 uv, float z, float r, sampler2D map ){
	float sum = 0.0f;
	for ( int i = 0; i < 8; ++i ){
		float2 offset = poissonDisk[i] * r;    
		float mapval = tex2D(map,uv+offset);
		float diff = z-mapval;
		sum += (diff>0);
	}
	return saturate(sum / 8.0);
}

void fshader(		
		in float4 l_shadowcoord : TEXCOORD0,
		in float4 l_lightclip : TEXCOORD1,
		in float l_back,
		uniform sampler2D k_depthmap : TEXUNIT0,
		out float4 o_color:COLOR
	)
{
	o_color = 0.0;
	if (l_back>0.0){
		float3 circleoffs = float3(l_lightclip.xy / l_lightclip.w, 0);
		float falloff = saturate(1.0 - dot(circleoffs, circleoffs));
		float4 proj = l_shadowcoord / l_shadowcoord.w;
	  
		float mapval = tex2D(k_depthmap,proj.xy);
		float diff = proj.z-mapval;
		float shade = (diff>0) ? PCF_Filter(proj.xy,proj.z,0.2*diff,k_depthmap) : 0;
		o_color = falloff * shade;
	}
} 

And here’s shadowcombiner.sha

//Cg
//
//Cg profile arbvp1 arbfp1

void vshader(
		float4 vtx_position : POSITION, 
		float2 vtx_texcoord0 : TEXCOORD0,
		out float4 l_position : POSITION,
		out float2 l_texcoord0 : TEXCOORD0,
		out float2 l_texcoord1 : TEXCOORD1,
		uniform float4 texpad_render, 
		uniform float4x4 mat_modelproj
	)
{
	l_position=mul(mat_modelproj, vtx_position);
	l_texcoord0 = vtx_position.xz * texpad_render.xy + texpad_render.xy;
	l_texcoord1 = vtx_texcoord0;  
}

void fshader(
		float2 l_texcoord0 : TEXCOORD0,
		float2 l_texcoord1 : TEXCOORD1,
		out float4 o_color : COLOR,
		uniform float4 k_light,
		uniform sampler2D k_render : TEXUNIT0,
		uniform sampler2D k_shadow : TEXUNIT1
	)
{
	o_color  = saturate(tex2D(k_render, l_texcoord0) - 0.2 * k_light * tex2D(k_shadow, l_texcoord1));
}

Let me know if you have any problems getting it to work… and any improvements you come up with.

hi rollingt70,

I can run your sample code very well. Thank you for the nice work.

However, I cannot combine autoshading with your sample code. It only works with my scene that do not have render.setShaderAuto() called.

Would you mind posting also your sample program setting up the scene with the shadow manager ?

Hi rollingt70,

Great work!

The sample program would be great!!!

and by the way the FilterManager seems to be missing

Great work, but I don’t see the need for the extra (possibly expensive) rendering pass to combine the results. What about using a ColorBlendAttrib to subtract the result?

:blush: I forgot to import the FilterManager so it couldn’t be found :blush:

I’m trying to use pro-rsoft’s softshadow example with the shadowManager updates from rollingt70

and now I have the next problem

AttributeError: ShadowManager has no attribute ‘_SchadowManager__hardness’

nor Ambient; nor Fov; nor NearFar; nor light

please advice

Looks like the author is busy on something else.

This shadowManger is quite different from pro-rsoft one. You don’t need to set the ambient and the spot light information. You set up the scene as normal, with lights. You can control how the shadow is cast thru the castercam in shadowManager, e.g.: change the fov of the castercam:
self.sMgr.castercam.node().getLens().setFov(fov)

Hardness is not supported. I think it can be added by changing the showcombiner.sha:

void fshader(
      float2 l_texcoord0 : TEXCOORD0,
      float2 l_texcoord1 : TEXCOORD1,
      out float4 o_color : COLOR,
      uniform float4 k_light,
      uniform sampler2D k_render : TEXUNIT0,
      uniform sampler2D k_shadow : TEXUNIT1,
      uniform float4 k_props
   )
{
   o_color  = saturate(tex2D(k_render, l_texcoord0) - k_props.x * k_light * tex2D(k_shadow, l_texcoord1));
}

Set the “props” shader input to control the hardness.

[/code]

Hello, I run the shadowmanager with following code, but when i turn autshade on, it doesn´t work. What am i doing wrong, or is it not possible to have shadows and autoshade to work together correctly?
I´m tryed to get shadows and diffuse/normalmaps/glossmaps very hard, i tryed evry code in the forum that i find (many sleepness nights), but no succes. So if somebody can confirm that what i´m trying todo is not possible, or how it can be done i would be veeeeery thankfull!

#!/usr/bin/env python

from pandac.PandaModules import *
loadPrcFileData("", "prefer-parasite-buffer #f")
#loadPrcFileData("", "show-frame-rate-meter #t")
from direct.directbase import DirectStart

# Import whatever classes are necessary

from direct.interval.IntervalGlobal import Sequence
from sys import exit

# Import the Shadow Manager
from shadowManager2 import ShadowManager

class World(object):
  def __init__(self):
    
    base.accept("v", base.bufferViewer.toggleEnable)
    base.accept("V", base.bufferViewer.toggleEnable)
    # Make a way out of here!
    base.accept("escape", exit)

    # Initiate the shadows
    self.sMgr = ShadowManager(render)
    
    #create lights:
    dlight = DirectionalLight('dlight')
    alight = AmbientLight('alight')
    dlnp = render.attachNewNode(dlight.upcastToPandaNode()) 
    alnp = render.attachNewNode(alight.upcastToPandaNode())
    dlight.setColor(Vec4(0.8, 0.8, 0.5, 1))
    alight.setColor(Vec4(0.2, 0.2, 0.2, 1))
    dlnp.setHpr(0, -60, 0) 
    render.setLight(dlnp)
    render.setLight(alnp)

    # Create the room
    
    self.room = loader.loadModel("models/abstractroom.egg")
    self.room.setTwoSided(True)
    self.room.setShaderAuto()
    self.room.reparentTo(render)
    
    
    

    # Load the teapots
    self.teapot = loader.loadModel("teapot")
    self.teapot.setTwoSided(True)
    self.teapot.reparentTo(render)
   
    self.teapot2 = loader.loadModel("teapot")
    self.teapot2.setTwoSided(True)
    self.teapot2.reparentTo(render)
    self.teapot2.setPos(0,5,5)

    # Set intervals to move the teapot
    self.teapot.hprInterval(5.0, Vec3.zero(), Vec3(360, 0, 0)).loop()
    Sequence(self.teapot.posInterval(2.0, Point3.zero(), Point3(2, 0, 1)), self.teapot.posInterval(2.0, Point3(2, 0, 1), Point3.zero())).loop()

    # Setup the camera
    base.disableMouse()
    camPivot = render.attachNewNode("cameraPivotPoint")
    base.cam.reparentTo(camPivot)
    base.camLens.setNearFar(1,1000)
    base.camLens.setFov(75)
    base.cam.setPos(-10,-10,15)
    base.cam.lookAt(self.teapot)
    
    # Setup an interval to rotate the camera around camPivot
    camPivot.hprInterval(15.0, Vec3.zero(), Vec3(360, 0, 0)).loop()
    
    # Position the shadow camera
    
    self.sMgr.changeLightPos((0,20,15), self.room.getPos())
    
    

# Initiate the world class and run!
World();run()

Are you using 1.5.4 ?

In 1.5.4, the filter manager has this comment:
* Automatic shader generation is enabled by default for
the main camera. You can override this by setting
shaders on individual nodes, or on the root of your
scene graph, but if you do, your own shaders need to
generate the outputs that the filter manager is expecting.

So, you can just use the shadow manager without calling setShaderAuto.
Check on my demo:
discourse.panda3d.org/viewtopic.php?t=5915

The ODE with Shadow Manager 2, it mixes this shadow manager with the scene which has normal map defined.

I am not sure about the behavior of 1.6.0

I’m using 1.6, so this could be the problem, i try again with 1.5.4, thanks in advance for this advice!

On 1.6 if i don’t setShaderAuto idon’t see a normal map applyed in ur example, but i try with 1.5.4…

edit:I tryed the shadowmanger2 with panda1.5.4 and it works!!! This is realy great, damn i should have posted this much earlyer, that would have saved me some nights ^^

edit2:so why isn’t that technique not working on 1.6??? What has to be changed to get it working there?

I wan´t to use webcam video in the app i’m planig to do and there seems to be a problem with webcamvideo and panda v1.5.4 (see (post 9): www1.panda3d.org/phpbb2/viewtopic.php?t=6069

But its´relay nice to see that normalmaps and shadows are working together, i was realy desperated because of this.
you made my day, no my week ^^

greetingz

Can anyone point out the exact files and shaders that make this work?

I have been given an error code"Shadow manager has no instance ‘changeLightPos’" when I try to run Sundaycoders version. Can anybody help?

For the record, the current CVS version of Panda has fully automatic shadows support that are compatible with the Shader Generator, that is at least 4x as efficient as the implementation on this thread.

E-mail me if you want a development snapshot build.