Sample: using directional lights+shadows effectively

Using directional lights with shadowing can often be a stumbling block for Panda users. Fitting the frustum around the scene properly can be tricky. So here is a little sample program that shows how to use directional lights effectively.

It includes a function that will tightly fit the frustum around the scene, for static, smaller scenes (press 1 to use it automatically). This is great for smaller scenes or where the light is static and there is no time-of-day, because it is slow to compute this and shouldn’t be used at runtime for more complex scenes. It’s delightfully simple, though:

    bmin, bmax = scene.get_tight_bounds(sun_path)
    lens = sun.get_lens()
    lens.set_film_offset((bmin.xz + bmax.xz) * 0.5)
    lens.set_film_size(bmax.xz - bmin.xz)
    lens.set_near_far(bmin.y, bmax.y)

It also includes a function (press 2) that is meant to dynamically adjust the frustum around both the scene and the area that is currently in view. It is not as tightly fitting as the first method; a much better implementation is warranted, but this may be a good starting point. It will try to make sure that areas you are not looking at are not shadowed, and that the shadow camera is even completely disabled if you are looking away from the scene.

It uses a GLSL shader that uses PBR lighting models to apply the shadows and lighting, though the shader is optional.

Keybinds:

 tab: toggle buffer viewer
 f12: save screenshot
 o: enable OOBE mode
 1: enable static adjustment mode
 2: enable dynamic adjustment mode
 3: disable automatic adjustment

directional-lighting.zip (1.9 MB)

12 Likes

Thank you for this :ok_hand:

Hi rdb,

I run this example in python and it works great. I’m trying to achieve a similar result in C++, so I started from a very simple scene with a ground plane and the teapot model. I setup a directional light and enabled ‘set_shader_auto()’ on the ‘get_render()’ but I’m not seeing the shadow.
This is the result I get, what am I missing?

Here is my code:

key a/z iincrease/decrease the light
key s calls the callback2 function which “should” enable/disable shadows

Thanks for any help

#include <pandaFramework.h>
#include <pandaSystem.h>
#include <cardMaker.h>

#include "directionalLight.h"

#include "load_prc_file.h"      // To handle Panda Config file

NodePath model_np{"model"};
LColor direct_color  = LColor(0.5, 0.5, 0.5, 1.0f);

DirectionalLight *dlight;

PandaFramework framework;
WindowFramework* windowFramework;

void callback(const Event* eventPtr, void* data)
{
    std::cout << __PRETTY_FUNCTION__ << std::endl;

    float *diff = (float*)data;
    direct_color  += LColor(*diff, *diff, *diff, 0.0);

    dlight->set_color(direct_color);
}

bool toggle{true};

void callback2(const Event* eventPtr, void* data)
{
    if(toggle)
    {
        std::cout << "enable shader " << std::endl;
        dlight->set_shadow_caster(true, 512, 512);
        dlight->mark_used_by_auto_shader();
        windowFramework->get_render().set_shader_auto();
    }
    else
    {
        std::cout << "disable shader " << std::endl;
        windowFramework->get_render().clear_shader();
    }

    toggle = !toggle;
}


int main()
{
    // open a new window framework
    framework.open_framework();

    load_prc_file_data("", "show-frame-rate-meter 1");      // enable frame rate meter
    load_prc_file_data("", "framebuffer-multisample 0");    // enable antialising
    load_prc_file_data("", "multisamples 0");               // enable antialising
    load_prc_file_data("", "sync-video 1");                


    // set the window title to My Panda3D Window
    framework.set_window_title("Shadow test C++");

    // open the window
    windowFramework = framework.open_window();
    windowFramework->setup_trackball();
    windowFramework->enable_keyboard();

    //
    // setup scene
    //

    NodePath scene = windowFramework->get_render().attach_new_node("scene");

    // Flat ground
    CardMaker flat_ground("floor");
    flat_ground.set_frame({-10.0, -10.0, 0.0}, {10.0, -10.0, 0.0}, {10.0, 10.0, 0.0}, {-10.0, 10.0, 0.0});
    PT(PandaNode) flat_ground_node = flat_ground.generate();
    NodePath flat_ground_np = scene.attach_new_node(flat_ground_node);
    flat_ground_np.set_color(0.8, 0.8, 0.8, 1.0);
    flat_ground_np.set_two_sided(true);

    // Load model
    NodePath model_loader  = windowFramework->get_panda_framework()->get_models();
    Filename pandafile = Filename::from_os_specific("teapot.egg");
    model_np = windowFramework->load_model(model_loader, pandafile);
    if(model_np.is_empty())
    {
        std::cerr << "Cannot load model " << std::endl;
        return -1;
    }
    model_np.reparent_to(scene);
    model_np.set_pos(0.0, 0.0, 0.0);
    model_np.set_two_sided(true);


    NodePath camera_group = windowFramework->get_camera_group();
    camera_group.set_pos(0, 0, 10);
    camera_group.set_hpr(0, -30, 0);

    // lights
    NodePath light_group = camera_group.attach_new_node("lights");

    dlight = new DirectionalLight("directional");
    dlight->set_color(direct_color);
    NodePath directional_light = light_group.attach_new_node(dlight);
    directional_light.set_hpr(-10, -20, 0);
    dlight->get_lens()->set_near_far(1, 30);
    dlight->get_lens()->set_film_size(20, 40);
    dlight->show_frustum();
    dlight->set_shadow_caster(true, 4096, 4096);

    // apply light to model
    model_np.set_light(directional_light);

    // setup automatic shadow handler
    callback2(nullptr, nullptr);

    // keyboard shortcut to increase/descrease lighting
    float inc =  0.05;
    float dic = -0.05;

    framework.define_key("a", "more light", callback,  &inc);
    framework.define_key("z", "less light", callback,  &dic);
    framework.define_key("s", "shadows",    callback2, nullptr);

    // do the main loop, equal to run() in python
    framework.main_loop();

    // close the window framework
    framework.close_framework();

    return 0;
}

Did you build Panda3D from source yourself? If so, did you enable Cg shader support?

Ok, thanks rdb.
I built source code using CMake on Linux (ubuntu 20.04), in che CMake cache I see

 HAVE_CG                          OFF                                                                                                                                                                                                                                            
 HAVE_CGD3D9                      OFF                                                                                                                                                                                                                                            
 HAVE_CGGL                        OFF  

So you are right, it was not enabled :slight_smile: My bad, sorry!
Now I followed the instructions and installed Gc NVidia library. (btw it seems it is abandoned by NVidia, is it ok to still rely on it?)

I have recompiled Panda3D, and now I see

 HAVE_CG                          ON           (hurrà)                                                                                                                                                                                                                           

Now I see self-shadows, although a bit edgy (see pic)

probably it’s a matter of tuning the parameters of these calls?

dlight->get_lens()->set_near_far(1, 30);
dlight->get_lens()->set_film_size(20, 40);
dlight->set_shadow_caster(true, 4096, 4096);

Anyway, I’m still missing the shadows the teapot shall cast on the white ground floor, which is the one I’m more interested in.

Any idea on that?
thanks

Sorry to bother again.

I’m still struggling with this shadow issue. As shown in the previous post I’m able to display self-shadows (i.e. shadows the teapot is projecting ont itself). What I’d like to have are the shadows the teapot should cast on the bottom plane. The plane is done simply using a CardMaker.

Does the plane need to have a material on it in order to ‘receive’ shadows from the teapot? Is there some other requirements? I’m not expert on this topic, so maybe I’m missing basic stuff.

Any hint / pointer is welcome,
thanks guys

Yes, the plane needs to have the same shader applied in order to receive shadows from the teapot.

The resolution of the shadows will improve if you make the light frustum smaller; you can use show_frustum() to show a visual aid of how large it is and how small you can afford to make it.

Hi @rdb! I see you mention that the shader is optional. How to cast the shadow onto the floor when using the built in shader? I copied some parts of the code in your .zip and put into my project. I like the sun and I am happy with the way things look, but I can’t seem to get the shadow to project onto the plane (a street model that is .bam).

The necessary parts from my constructor are as follows:

from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from panda3d.core import KeyboardButton, AmbientLight
from panda3d.core import DirectionalLight, WindowProperties
from panda3d.core import load_prc_file_data, Vec3, VBase3, VBase4
from components.ninja import Ninja
import simplepbr


config = """
multisamples 2
framebuffer-srgb #t
bounds-type best
"""
load_prc_file_data('', config)


class Panda3dStreet(ShowBase, Ninja):

    # noinspection PyArgumentList
    def __init__(self):
        ShowBase.__init__(self)
        simplepbr.init()
        Ninja.__init__(self)
        props = WindowProperties()
        props.setTitle('panda3d street')
        self.win.requestProperties(props)

        self.street = self.loader.load_model('assets/wet_intersection_american_road.bam')
        self.street.reparent_to(self.render)
        self.set_background_color(0, 0, 0, 1.0)

        amb_light = AmbientLight('sky')
        amb_light.set_color(VBase4(skycol * 0.04, 1))
        amb_light_path = self.render.attach_new_node(amb_light)
        self.render.set_light(amb_light_path)

        sun = DirectionalLight('sun')
        sun.set_color_temperature(6000)
        sun.color = sun.color * 4
        sun_path = self.render.attach_new_node(sun)
        sun_path.set_pos(10, 28, 3)  # use this to change position of sun
        sun_path.look_at(0, 0, 0)
        sun_path.hprInterval(10.0, (sun_path.get_h(), sun_path.get_p() - 360, sun_path.get_r()), bakeInStart=True).loop()
        self.render.set_light(sun_path)

        # Enable shadows; we need to set a frustum for that.
        sun.get_lens().set_near_far(1, 30)
        sun.get_lens().set_film_size(20, 40)
        # sun.show_frustum()
        sun.set_shadow_caster(True, 4096, 4096)

        self.disable_mouse()

        self.street.set_scale(0.1, 0.1, 0.1)
        self.street.set_pos(0, 0, 0)

        self.street.set_light(sun_path)

        self.ninja.reparent_to(self.render)
        self.ninja.set_pos(Vec3(0, 0, 0.6))

    # ... ... camera handling goes here

I left the hprInterval part in there to see if shadows would appear as the light changes. I also tried to cast the shadow onto the street by adding self.street.set_light(sun_path).



I can see the shadows changing on the ninja, but how to project the ninja’s shadow onto the street? Thank you for the support!

1 Like

oh adding simplepbr.init(enable_shadows=True) did exactly what I was looking for. Stumbled across that one accidentally.

1 Like