0
\$\begingroup\$

A Quick Note

This question assumes you have some knowledge about the overall rendering process and do not need clarification on rendering objects with HLSL and a DeviceContext. I use the SharpDX libraries to write my code in C# but I can read C++ and convert it if needed.


Overview

I am currently trying to create a prefab for a point light in my engine at work. I currently have a working directional light and can adapt from that if needed. Originally this post included some code from a pixel shader that I found on StackOverflow, but @DMGregory pointed out issues with it. I also attempted to follow a pixel shader supplied in the book Real-Time 3D Rendering with DirectX and HLSL (chapter 7 page 117); however, the HLSL in that seems to be a bit outdated since I can't use the following functions:

  • lit
  • get_vector_color_contribution
  • get_scalar_color_contribution

These seem to be no longer supported with ps_5_0 (the HLSL won't compile) and I can't find any documentation that provides updated equivalents. The exact error message during compilation is:

error X3004: undeclared identifier 'get_vector_color_contribution`.

This same message is presented with all three functions.

When I attempted the pixel shader found on StackOverflow, it didn't do what I expected. I eventually discovered that this is because it wasn't sampling textures. Correcting that is simple enough, but this would have to be applied per object and makes it difficult for multiple point lights. This doesn't really fit my idea of simplicity.


My Idea

I would prefer to create a point light shader that is applied to all objects capable of being illuminated in a single pass; however, I fear this isn't possible or would be incredibly difficult to achieve.

Since it is a lot of code to show the entire process, I will just include a couple relevant samples and hopefully that will help shed some light on what I currently do when rendering objects:

public class SceneObject {
    public Vector3 Position { get; set; }
    public PixelShader PixelShader { get; set; }
    public virtual void Render() { }
}
public class PointLight : SceneObject {
    public float Radius { get; set; }
    public float Intensity { get; set; }
    public Color Diffuse { get; set; }
    public Color Ambient { get; set; }
    public Color Specular { get; set; }
}
public class Scene {
    private void ChangeShader(SceneObject obj, bool setToCurrent) {
        if (setToCurrent)
            currentPixelShader = context3D.PixelShader.Get();

        if (currentPixelShader.DebugName != obj.PixelShader.DebugName)
            context3D.PixelShader.Set(obj.PixelShader);
    }
    public void Render(SceneObject obj, bool renderChildren = true) {
        if (!obj.Enabled)
            return;

        if (obj is PointLight)
            SetPointLightBuffer((PointLight)obj);

        if (obj.PixelShader != null)
            ChangeShader(obj, false);

        SetObjectBuffer(obj);

        if (obj.HasTexture)
            UpdateMaterialBuffer(true);

        obj.Render();

        if (obj.HasTexture)
            UpdateMaterialBuffer(false);

        if (renderChildren)
            foreach (SceneObject child in obj.Children)
                Render(child);
    }

    ChangeShader(obj, true);
}

There are things missing from the code I've supplied above, but this is intentional as it is a lot of code. Each object renders itself in the appropriate way. The Scene class manages the DeviceContext and other important things such as the constant buffers used by the shaders, this allows greater flexibility for future developers instead of having to learn all of the underlying DirectX code, they can focus on creating objects with prefabs instead.


Goal

The overall goal is to achieve the most simplistic implementation of a point light that I can get, to where any object that is capable of being illuminated is illuminated when in range of the light. I believe this is going to be a post process pixel shader, but I could be wrong.


Question

What is the step by step (proper) process for rendering a point light object into a scene?


Note

I don't really need code samples to answer this question unless the supplied shader is incorrect. I can figure the code out, so long as you're detailed enough about the process in your answer.

\$\endgroup\$
8
  • \$\begingroup\$ "I couldn't get it to work" - can you show us specifically what you tried, and what went wrong? Did you get a compile error or runtime error? Or a different output than you were expecting? (In what specific way did it differ?) It's also not immediately clear what kind of renderer / pass you're creating — forward, deferred, forward+, light pre-pass, etc., so can you tell us more about where you want this shader to fit into your overall rendering pipeline? \$\endgroup\$
    – DMGregory
    Nov 20, 2018 at 15:52
  • \$\begingroup\$ @DMGregory I've updated the post to hopefully help clarify everything; the code is as minimal as I could make it as I assume some knowledge of rendering is understood by answerers. \$\endgroup\$
    – user121635
    Nov 20, 2018 at 16:25
  • \$\begingroup\$ What specific error do you get when trying to use get_vector_color_contribution as defined here in your shader code? It doesn't look like it uses anything unusual that would be unsupported, just a little multiplication. \$\endgroup\$
    – DMGregory
    Nov 20, 2018 at 16:31
  • \$\begingroup\$ @DMGregory Updated the post to display that. \$\endgroup\$
    – user121635
    Nov 20, 2018 at 16:44
  • \$\begingroup\$ This is telling you that you haven't defined the function get_vector_color_contribution anywhere in the shader code you've tried to compile. It's not a built-in function, just a regular user-defined function. Have you tried declaring the function like the error message tells you to? \$\endgroup\$
    – DMGregory
    Nov 20, 2018 at 16:46

1 Answer 1

0
\$\begingroup\$

It looks like you're trying to just throw your light in with a jumble of other scene objects, rendered in some arbitrary order without any knowledge of one another.

Because illumination isn't an "object" per se, but an effect on the surfaces of other objects, it needs to be handled separately. Either the objects need to know about the lights, or the light needs to be able to query information about the objects.

There are two common routes to incorporating lighting: forward and deferred. Lots more specialized variants have been developed based on these, but knowing these core concepts will give you a foundation for understanding the other techniques.

Forward Rendering

In Forward Rendering, you incorporate illumination as you render each object.

So the shader for the object will expose some uniforms (or reference a data buffer) containing information about the lights shining on that object. In your rendering loop you'd check which lights illuminate each object, and populate those variables/buffers before rendering it. Then the object's shader will...

  • (vertex shader)
    • transform the model's vertices into device coordinates
    • compute lighting vectors for each of the lights shining on the object
  • (pixel shader)
    • sample the object's surface colour, normal map, and other textures
    • shade the result for each light
    • sum the contributions of each light into a final output colour

Advantages with this method include that it works for transparent geometry just as well as it does for opaque stuff, and you can have highly custom shader logic for each object, mixing wildly different material effects.

Disadvantages include, as you point out, it's difficult to scale to many lights. Each pass will typically have a maximum number of lights it can handle (eg. 4). When more lights affect the object, then you either need to exclude the less important lights (which can show odd discontinuities), approximate extra lights with a simpler form like spherical harmonics, or render additional passes to add-up the contributions of every light. The other downside is that overdraw can be quite high with this approach - you might spend several passes rendering lighting on one polygon, only to later cover up that polygon with a closer one that needs its own lighting.

Deferred Rendering

In Deferred Rendering, you put off lighting until the end.

As you render each object, you're not rendering its final appearance, but rather making notes for the later lighting passes to use. Typically you'll use multiple render targets collectively called a "G-Buffer" to record, for each pixel on the screen...

  • Depth
  • Normal direction
  • Albedo colour
  • Material properties like roughness/metalness/specularity (the exact details will depend on your reflectance model)

After this G-Buffer is completely populated with all the opaque objects in the scene, you can use it to compute the light reflecting off each pixel to "splat" illumination additively onto a black-cleared background,.

For each light, you render a piece of geometry representing the light's bounds, to ensure you run the lighting shader only on the pixels the light could potentially touch (this helps control overdraw when you have lots of small lights). For each pixel it covers, you read the G-buffer and decode the world position of the surface at that pixel, shading it using the current light and outputting the light bouncing off that surface pixel toward the camera for just the one light. Adding up all those passes gives you the complete light bouncing off that pixel & reaching the camera.

Advantages with this method include that it partly decouples the complexity of your scene from the complexity of your lighting. Your lighting cost is bounded by the on-screen pixel area of your lights, rather than exploding with the vertex count of your scene. This makes it more efficient to render lots and lots of lights in high-detail scenes.

Disadvantages include that this works only for opaque geometry, since it assumes there's just one (front-most) surface visible at each pixel. Deferred renderers will often include a forward pass to handle transparent objects after the opaque scene has been fully lit. It's also more complicated to handle wildly different material logic per object this way, since the shading pass is separated from the object (typically we'll try to boil down lots of material behaviours into a set of parameters for a shared reflectance model).

\$\endgroup\$

You must log in to answer this question.