Directionnal light + shadows

I’ve seen a few people ask for it, so I put a snippet here. It is per vertex lighting, so the specular will be a bit weird. The shadows are parallel I think.

http://www.sendspace.com/file/r092xo

It needs panda3D devel version.
It wont work on GM45 :frowning:

EDIT : added per pixel lighting + no light falloff so it looks like sun.


[color=green]PER PIXEL LIGHTING NO FALLOFF :

//Cg

// VERTEX SHADER //
void vshader(
            float4 vtx_position  : POSITION,
            float3 vtx_normal    : NORMAL,
            out float4 l_position : POSITION,
            out float4 l_color     : COLOR,
            out float4 l_shadowcoord : TEXCOORD1,
	        out float4 l_lightclip : TEXCOORD3,
	        out float4 l_modelpos : TEXCOORD4,
	        out float3 l_normal : TEXCOORD5,
	        uniform float3 k_scale,
	        uniform float4 k_push,
	        uniform float4x4 trans_model_to_clip_of_light,
	        uniform float4x4 mat_modelproj
            )

{

    float4 position = vtx_position * float4(k_scale,1);

    l_position = mul(mat_modelproj, position); 
    
    l_modelpos = position;
    
    l_normal = vtx_normal;

    // calculate light-space clip position.
    float4 pushed = position + float4(vtx_normal * k_push, 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);
}

// PIXEL SHADER //
void fshader(
             uniform sampler2D k_shadowmap : TEXUNIT3,
             uniform float4 k_push,
             uniform float3 k_specular,
             uniform float4x4 trans_clip_to_world,
             uniform float4x4 trans_model_to_world,
             uniform float4x4 trans_model_to_clip_of_light,
             uniform float3 k_globalambient,
             uniform float3 k_lightcolor,
             uniform float4 wspos_A,
             uniform float4 wspos_B,
             uniform float4 wspos_camera,
             uniform float3 k_scale,
             in float4 l_color : COLOR,
             in float4 l_shadowcoord : TEXCOORD1,
             in float  l_smooth : TEXCOORD2,
             in float4 l_lightclip : TEXCOORD3,
             in float4 l_modelpos : TEXCOORD4,
             in float3 l_normal : TEXCOORD5,
             out float4 o_color : COLOR0)
{

    float3 P; // point being lit
    float3 N; // surface normal
    float3 L; // light ray vector
    float3 V; // vector toward the viewpoint
    float3 H; // vector halfway between V and L
    
    float diffuse_light; // amount of diffuse light per vertex
    float specular_light; // amount of specular light per vertex
    
    // transformations into world space
    P = mul(trans_model_to_world, l_modelpos); 
    N = normalize(mul(float3x3(trans_model_to_world), l_normal)); 

    // calculate diffuse light
    L = normalize(wspos_A - wspos_B);
    diffuse_light = max(dot(N, L), 0)  ;

    // calculate specular light
    V = normalize(wspos_camera - P);
    H = normalize(L + V);
    specular_light = pow(max(dot(N, H), 0), k_specular.x);
    if (diffuse_light <= 0) specular_light = 0;
    
    float3 circleoffs;
    float falloff;
    float shade;

    // calculate light falloff
    circleoffs = float3(l_lightclip.xy / l_lightclip.w, 0);
    falloff = saturate(1.0 - dot(circleoffs, circleoffs));

    // calculate shadows projection
    shade = tex2Dproj(k_shadowmap,l_shadowcoord);

    // final output 
    o_color.xyz = diffuse_light * k_lightcolor + specular_light * k_lightcolor + k_globalambient;
    if(falloff > 0)o_color *= shade;
    
}


[color=green]PER VERTEX LIGHTING :

//Cg

// VERTEX SHADER //
void vshader(
            float4 vtx_position  : POSITION,
            float3 vtx_normal    : NORMAL,
            out float4 l_position : POSITION,
            out float4 l_color     : COLOR,
            out float4 l_shadowcoord : TEXCOORD1,
	        out float4 l_lightclip : TEXCOORD3,
            uniform float4 k_push,
            uniform float3 k_scale,
            uniform float3 k_specular,
            uniform float4x4 mat_modelproj,
            uniform float4x4 trans_model_to_world,
            uniform float4x4 trans_model_to_clip_of_light,
            uniform float3 k_globalambient,
            uniform float3 k_lightcolor,
            uniform float4 wspos_A,
            uniform float4 wspos_B,
            uniform float4 wspos_camera)

{

    float4 position = vtx_position * float4(k_scale,1);

    l_position = mul(mat_modelproj, position); 

    float3 P; // point being lit
    float3 N; // surface normal
    float3 L; // light ray vector
    float3 V; // vector toward the viewpoint
    float3 H; // vector halfway between V and L
    
    float diffuse_light; // amount of diffuse light per vertex
    float specular_light; // amount of specular light per vertex
    
    // transformations into world space
    P = mul(trans_model_to_world, vtx_position); 
    N = normalize(mul(float3x3(trans_model_to_world), vtx_normal)); 

    // calculate diffuse light
    L = normalize(wspos_A - wspos_B);
    diffuse_light = max(dot(N, L), 0)  ;

    // calculate specular light
    V = normalize(wspos_camera - P);
    H = normalize(L + V);
    specular_light = pow(max(dot(N, H), 0), k_specular.x);
    if (diffuse_light <= 0) specular_light = 0;

    l_color.xyz = diffuse_light * k_lightcolor + specular_light * k_lightcolor + k_globalambient;
    l_color.w = 1;

    // calculate light-space clip position.
    float4 pushed = position + float4(vtx_normal * k_push, 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);
}


// PIXEL SHADER //
void fshader(
             uniform sampler2D k_shadowmap : TEXUNIT3,
             in float4 l_color : COLOR,
             in float4 l_shadowcoord : TEXCOORD1,
             in float  l_smooth : TEXCOORD2,
             in float4 l_lightclip : TEXCOORD3,
             out float4 o_color : COLOR0)
{
    float3 circleoffs;
    float falloff;
    float shade;

    // calculate light falloff
    circleoffs = float3(l_lightclip.xy / l_lightclip.w, 0);
    falloff = saturate(1.0 - dot(circleoffs, circleoffs));

    // calculate shadows projection
    shade = tex2Dproj(k_shadowmap,l_shadowcoord);

    // final output 
    o_color = l_color * shade * falloff ;
    
}

.py :

##################################################### 
##         Directionnal Light Shadows              ## 
##################################################### 
## 23/09/11
## by Manou

### IMPORTS ###
from panda3d.core import *
loadPrcFileData('', 'framebuffer-stencil 0')
loadPrcFileData('', 'compressed-textures 0')
loadPrcFileData('', 'show-buffers 0')
loadPrcFileData('', 'basic-shaders-only 1')
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import Vec3,Vec2
from pandac.PandaModules import Lens
from math import pi,sin,cos
from direct.actor.Actor import Actor

# CONSTANTS #
LIGHT_COLOR = Vec3(1.0,0.7,0.3)
GLOBAL_AMBIANT = Vec3(.1,.1,.1)
SHADOW_MAP_SIZE = 512
FILM_SIZE = Vec2(1000,1000)
SHADOW_BIAS = Vec3(10,10,10)

class Dirlight(DirectObject):
    
    ### INIT ###  
    def __init__(self):
        
        self.setup_scene()
        self.setup_buffers()
        self.setup_cameras()
        self.setup_shaders()
        
        taskMgr.add(self.update,"update")
                  
    ### SCENE SETUP ###  
    def setup_scene(self):
    
        self.scene = render.attachNewNode("scene")
        
        # light direction A => B
        self.A = loader.loadModel("sphere_1")
        self.A.reparentTo(render)
        self.A.setPos(-100,0,20)
        self.A.setScale(1)
        self.A.setColor(1,0,0)
        
        self.B = loader.loadModel("sphere_1")
        self.B.reparentTo(render)
        self.B.setPos(0,0,10)
        self.B.setColor(0,1,0)
        self.B.setScale(1)
        
        # ground
        self.ground = loader.loadModel("grass_anim")
        self.ground.reparentTo(self.scene)
        self.ground.node().setBounds(OmniBoundingVolume())
        self.ground.node().setFinal(1)
        
        # cubes
        self.cubes = []
        for i in range(10): 
            angle_degrees = i * 36 
            angle_radians = angle_degrees * (pi / 180.0) 
            cube = loader.loadModel("cube")
            cube.reparentTo(self.scene)
            cube.setPos(sin(angle_radians) * 40, cos(angle_radians) * 40, 20)
            cube.node().setBounds(OmniBoundingVolume())
            cube.node().setFinal(1)
            self.cubes.append(cube)
    
    ### CAMERAS SETUP ###
    def setup_cameras(self):
        
        base.cam.node().getDisplayRegion(0).setClearColor(Vec4(.5, .5, .5, 1))
        base.cam.node().getDisplayRegion(0).setClearColorActive(1)
        
        self.shadow_cam = base.makeCamera(self.shadow_buffer)
        self.shadow_cam.reparentTo(self.A)
  
        lens = OrthographicLens()
        lens.setFilmSize(FILM_SIZE)
        
        self.shadow_cam.node().setLens(lens)
        self.shadow_cam.node().getDisplayRegion(0).setClearColor(Vec4(0, 0, 0, 1))
        self.shadow_cam.node().getDisplayRegion(0).setClearColorActive(1)
        
    ### BUFFERS SETUP ###
    def setup_buffers(self):
    
        shadow_map_temp = Texture()
        self.shadow_buffer = base.win.makeTextureBuffer('shadow', SHADOW_MAP_SIZE, SHADOW_MAP_SIZE, shadow_map_temp)
        
        self.shadow_map = Texture()
        self.shadow_map.setMinfilter(Texture.FTShadow)
        self.shadow_map.setMagfilter(Texture.FTShadow)
        
        self.shadow_buffer.addRenderTexture(self.shadow_map, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPDepth)
    
    ### SHADERS SETUP ###
    def setup_shaders(self):
        
        # PARAMS : object, specular, scale #
        
        # apply light to ground
        self.apply_light(self.ground, 30, Vec3(1000,1000,1000))
        
        # apply light to cubes 
        for i in range(10):             
            self.apply_light(self.cubes[i], 30, Vec3(2,2,100))

    ### LIGHT FUNCTION ###
    def apply_light(self, obj, specular, scale): 
        
        # apply light shader
        obj.setShader(Shader.load("dir_light.cg"))
        
        # per object
        obj.setShaderInput('specular', specular)
        obj.setShaderInput('scale', scale)
        
        # all objects
        obj.setShaderInput('push', SHADOW_BIAS )
        obj.setShaderInput('globalambient', GLOBAL_AMBIANT)
        obj.setShaderInput('lightcolor', LIGHT_COLOR)
        obj.setShaderInput('light', self.shadow_cam)
        obj.setShaderInput('A', self.A )
        obj.setShaderInput('B', self.B ) 
        obj.setShaderInput('camera', base.camera)
        obj.setShaderInput('shadowmap', self.shadow_map)
                    
    ### UPDATE PER FRAME ###
    def update(self,task):
        
        # tests
        self.A.setZ(self.A.getZ()+cos(task.time)/20)
        self.A.setY(self.A.getY()+cos(task.time)/20)
        
        # shadow cam always looking at B
        self.shadow_cam.lookAt(self.B)
        
        return task.cont
    
app = Dirlight()
run()

Thanks, that looks great. But it fails for me since it can’t find the textures ./tex/panda2.jpg or terre.jpg. Did you forget to zip them up or fix their pathnames, or do I need an even more recent Panda devel version (mine is a devel snapshot from a few weeks ago)? (It does run and display something, but nothing like your screenshots. :confused: )

Mmmmm, that’s weird, the textures are not needed in this example, it’s because the code is modified from one of my other project.

Can you post a screenshot of the result you have ? (with buffers display enabled, if possible).

What’s even weirder is that when I run it again, it no longer prints those error messages (though I still don’t have the texture files). Does Panda somehow remember it had those errors in a prior run, and not repeat the error message?!?

The code does indirectly refer to those texture files, via egg files that you included, like this:

% fgrep terre *
grass_anim.egg: “terre.jpg”

% fgrep panda2 *
sphere_1.egg: “./tex/panda2.jpg”

% fgrep grass_anim *
dirlight.py: self.ground = loader.loadModel(“grass_anim”)

% fgrep sphere_1 *
dirlight.py: self.A = loader.loadModel(“sphere_1”)
dirlight.py: self.B = loader.loadModel(“sphere_1”)

So that probably explains why what I saw was not colored.

The reason I didn’t even recognize the scene was just the initial point of view – when it started, I saw this:

but after moving the camera around I see this:

I guess that is the right scene, but with no colors, and still not a good point of view to look for shadows.

I moved it around some more and saw this:

which may have some shadows in it (which seem to be moving), but without the colors it’s hard to see them.

BTW, your post had two shaders but your zip file had only one. I’m guessing the forum has the same code as the zip file, plus an alternative shader to try in place of the one in the zip file (and that the shaders correspond to the two images you posted). But I just ran ‘ppython dirlight.py’ in the directory I unpacked from the zip file – I didn’t use any code posted in the forum.

I’m not sure what you mean by “with buffers display enabled”.

If it matters, I’m on Mac OS 10.7, and the video chip is reported as “AMD Radeon HD 6630M 256 MB”.

My graphic card is Nvidia gtx 260, maybe the code for ATI is different.

Try this,

to display the buffers replace :

loadPrcFileData('', 'show-buffers 0')

with

loadPrcFileData('', 'show-buffers 1')

in the first lines of dirlight.py.

to check if the lighting is applying correctly replace

// final output 
o_color = l_color * shade * falloff ;

with

// final output 
o_color = l_color;

in dir_light.cg

Does the console prints shader compiling errors ?

FWIW, I also tried the shadow-related samples that come with Panda. Some of them worked, and some of them failed in various ways. I can provide details if anyone’s interested in looking into that. (The graphics chip is part of a new Mac Mini which Apple says is good enough for games, so it’s reasonably new and ought to be good enough for this, perhaps unlike the lowest-end Mac Mini, which has Intel graphics only and which Apple conspicuously doesn’t say is good for games.)

Ok, I did that, and I also played around with point of view to find a better one than before. I got this:

It looks to me like the shadows themselves are there and working well, but there is also a strong effect of “self-shadowing” (I’m guessing about this cause), by both the ground (causing much of it to be dark, and the serration artifacts on the edge of the dark area), and by the should-be-lit sides of the pillars that are casting shadows (note that their near sides ought to be brightest, but are dark).

(In my previous tries I had only found points of view which face into the dark part of the ground, so I hadn’t noticed the place where the main pillar shadows were falling.)

If this is the cause, then maybe I just need to tweak some sort of shadow depth offset parameters differently than you do for your system. (Still it would be interesting to understand the source of the difference, and necessary if we want portable code.)

That gets me this:

I am not sure whether the POV or lighting is equivalent to the other screenshots, or if the lighting is working correctly – the lighting changes when I change the point of view, and I don’t know if that’s a bug or if the lights are supposed to not move with the camera then. The ground color and brightness changes more than I’d expect if the lighting was “nice”, but if this is a single bright directional light, maybe that’s expected.

But at least the lighting is working well enough to show that the shadows are not working pefectly. That is, using the shader change which just shows lighting, it’s easy to find a POV which lights up some sides of the pillars, but they are never lit on the correct side when using shadows (though they are lit on the top).

No. (Nor does it do so for most of the shader sample code that comes with Panda. For the ones that fail, I forget now whether or not it prints any shader compiling errors.)

It just looks like the near and far clips of the camera are very poorly chosen.

Is there a reason that could affect my system and not his? For example, could our default depth buffer resolution be different?

Edit: maybe I should have said, default depth texture resolution.

As for near/far, thanks for the suggestion. I tried setting his shadow_cam’s lens near/far from their default of (1, 100000) to (0.1, 500). That looks better, but it gives two-ways shadows (going in both directions from the pillars), and the ground shading with “serration” was still there.

I tried again with (2.0, 500.0) but got the same result. (I am just guessing about reasonable values, but if I understand this right, these are close enough that the shadow’s depth resolution shouldn’t be an issue. But there is no guarantee I understand it right.)

Edit 2: I picked my edited near/far values above, assuming that the light source is “off to the side but not too far away”, like would make sense for a point source. Now I realize that since the light is directional, I can’t assume that – it might be anywhere. I don’t have time right now to read the setup code to see where it actually is, so those values may not make sense at all.

Manou,

I am wondering why you scale your model objects (by the 3-vector k_scale of unequal values) inside the vertex shader (rather than inside Panda3D scenegraph using setScale), and whether you’re doing it correctly. See code snippet above from the per-pixel variant (and subsequent code) – it looks like vtx_position gets scaled but vtx_normal never does.

I scale the object in the shader because it wasn’t working for me with panda setScale() (I think they do the same in the shadow sample, can’t check atm).

I think rdb is right, it’s a problem with near & far clips, it looks like the z-buffer precision varies from card to card http://www.sjbaker.org/steve/omniv/love_your_z_buffer.html, but maybe the information on this site was for old graphic cards.

What is strange is I never have to set the near & far clips in my codes, what are the default values panda is giving if there are any ?

Try to change this

loadPrcFileData('', 'basic-shaders-only 1') 

to 0.

Can someone with a nvidia card check if the code is working ?

About the light changing with camera point of view, it comes from the specular in vertex shading, because the models are very low poly.
EDIT : the light is moving too :smiley:

    # tests 
    self.A.setZ(self.A.getZ()+cos(task.time)/20) 
    self.A.setY(self.A.getY()+cos(task.time)/20) 

For me the default values are 1 and 100000.

Good idea, I will try that later.

FYI, as I mentioned already, only some of the Panda sample code about shadows was working for me (in 1.7.2). I tried them again as a comparison, in my panda development snapshot from a few weeks ago:

  • Tut-Shadow-Mapping-Basic.py hangs the process with no error message.

  • Tut-Shadow-Mapping-NoShader.py complains about “No ARB_shadow”.

  • Tut-Shadow-Mapping-Advanced.py works, but only for the initial setting of its ‘L’ toggle (move light source far or close). Otherwise it gives incorrect “striped shadows” of some kind. I added some print statements and found out it was falling back to shadow-nosupport.sha rather than the preferred shadow.sha.

  • experimental/Tut-Shadow-Mapping-Ext.py runs, but no shadows are visible.

That’s not the only cause, since I modified the code to stop anything from moving, and used the per-pixel version, and even then the lighting changes as I change the POV interactively. (But it does look like what changes might be all specular. I didn’t yet try turning off specular to find out.)

Here’s an update, for anyone who is following this discussion.

  • I reset ‘basic-shaders-only’ to 0 but it made no difference.

  • I fixed the “two-ways shadows” I was seeing with your (manou’s) code, by replacing the use of tex2Dproj in your shader in the same way as is done to make shadow-nosupport.sha from shadow.sha in the shadow tutorial sample code. Evidently that Cg subroutine exists and runs for me, but gives different results than it does on systems for which getSupportsShadowFilter returns True. (Interestingly, my system does seem to support the Texture.FTShadow filter in spite of that – at least using that filter on a shadowmap does something different than the default filter; I’m not sure it’s different than FTLinear.)

  • I agree that the tutorials all use the shaderInput called ‘scale’ rather than the usual setScale, and that setScale itself doesn’t seem to work with those shaders. I haven’t yet found any documentation of why setScale doesn’t work with them and whether this could be fixed. I’d like to know.

  • I looked into how the ‘scale’ shaderInput is used in the tutorials. In them, a uniform scale (same value in x, y, and z) is always supplied to it. The shader code scales the vertex positions (in both the shadow casting and shaded-light-applying shaders) but makes no attempt to scale normals. (If I understand correctly, what it ought to do, to be correct for nonuniform scale, is scale normal vectors the same way and then renormalize them.)

Not scaling normals won’t affect the shadow positions, but in the general case of a nonuniform scale applied to arbitrary geometry, the lighting values (l_smooth in the tutorial shaders, diffuse_light in yours) will be calculated wrongly.

But it doesn’t happen in the complete example code of the tutorials because their scenes only use uniform scales. Your scene does use a nonuniform scale, but only for a cube, which happens to have only axis-aligned faces for which scaling and then renormalizing the normals wouldn’t change them. But in general it would be incorrect to use the ‘scale’ shaderInput nonuniformly. (This limitation in the tutorial shaders should of course be documented somewhere (and the need for this shaderInput explained there), but if it is I haven’t found it.)

  • The per-pixel version of your shader was letting shadows turn off even ambient light, which they shouldn’t do. (The tutorial shaders were not doing that, but even they were assuming the non-shaded light had full intensity even if the ambient light had significant intensity.)

After fixing the use of tex2Dproj (and perhaps the ambient light issue) I did get correct-looking shadows from your code. Studying it and comparing it to the tutorial shaders was very educational, so thanks again for posting it.

I apologize for serious thread resurrection, but would you be interested in re-uploading your zip archive, Manou? The link on the OP no longer works, and since the problems I had with Panda 1.7.2 concerning shadows seem to be fixed, I’m eager to try your shader out again. :slight_smile:

Sure, here it is : http://dl.dropbox.com/u/13388237/Panda3D/dirlight.zip

Serious thread resurrection - again - but I just had to state that your snippet is working perfectly on my GM45 with Panda 1.8, Manou. Thanks again for posting this! :slight_smile:

Is the code from the OP still up to date?
If not, would anybody mind reuploading it?
I think it would also be a good idea to upload it to github or the like.

dl.dropbox.com/u/13388237/dirlight.zip

I reuploaded :smiley:
I needed some space on dropbox :arrow_right:

Maybe you should try http://p3dp.com - it’s a project/file-hosting space specifically for Panda projects.

Need something like this, anyone still have a copy of Manou’s sample zip file?

Thanks.

I found a copy I had lying around:
dl.dropboxusercontent.com/u/199 … rlight.zip

Although I’d probably point you to Tobias’ Renderpipeline, which would give you better lighting results.

github.com/tobspr/RenderPipeline