Struggling to find point light resources

Hello,

I’ve been a long-time user of Monogame, but this is my first foray into 3D with the framework. Thus far, I’ve got models on screen, WASD movement, and things are going well. I’m struggling to find resources that actually work with Monogame for point lighting. So far I’ve found decent shader files, but many don’t compile using the pipeline tools for one obscure reason or the other (All errors so far are due to features not yet implemented).

I’m reaching out to the community in the hopes that someone has working HLSL that will actually compile in the pipeline tool so that I can introduce point lighting into my project. Does anyone have A sample of this actually working within Monogame? I’m a huge fan and I’d hate to go over to Unity3D for this.

you have come to the right place friend.

First question though - do you have any experience with shaders yet? Or starting from scratch?

We’ll see where we can go from there, because if you can write a basic diffuse shader some point lights are easy to implement as well!

For basic light shader stuff here is a resource that will work with monogame for sure:
http://rbwhitaker.wikidot.com/hlsl-tutorials

If you understand that you can easily implement point lights with a few more lines.

If you have no clue about shaders at all AND do not care to learn, that’s fine too, I’m sure we can cook up something for you.

Thank you for the hospitality!!

I am willing to learn. I’ve been going through a few samples on ambient light shaders. Right now, they feel a bit like a blackbox that I’m putting information into, and “something” happens. I want to get to the point where I can at least write a few basic shaders.

I’ve chosen to create and UV map my models in Blender, and so far, I’ve noticed that all of my ambient shader code is just coloring over the textures. So I’ve been feeling my way through and I’ve discerned that there must be some way to control alpha for shading over top of models that have textures.

So long story short, right now, I wouldn’t mind finding something a little more complete, so that I can reverse engineer it. I feel crazy going through all of these tutorials because they often leave out one or two minor details which stop me from figuring out where to go.

I will give your link a solid read through! In the mean time, if I can give you any additional information, I’d be happy to supply it!

I was looking for some information on ray tracing yesterday and ran into https://github.com/LWJGL/lwjgl3-wiki/wiki/2.6.1.-Ray-tracing-with-OpenGL-Compute-Shaders-(Part-I)
It’s the best write up on computer graphics lighting I’ve ever read. Despite the title it’s quite general and not just about raytracing. If you just want to dig in this is not what you’re looking for, but if you really want to know what you’re doing it’s good to read this (or something similar). It’s mostly a write-up on how light works physically and the intuition on techniques that are used for simulating it on your computer. I think you can just scan through the raytracing specific stuff and still follow along if you’re not interested in that. Be sure to check out part 2 too. If you’ve read this at least intuitively you should know what your shader should do.

Another great resource: http://www.catalinzima.com/xna/tutorials/
Contains an HLSL crash course and a deferred rendering tutorial among others.

What kind of materials do you use for your models?

PBR (albedo, normal, roughness, metallic)? Only a texture? A texture plus normal map?

I’ve made a basic point light shader, here you go

// our inputs per model
matrix World;
matrix WorldViewProj;       //This is world * view * projection

// our inputs per change in lighting / draw

float3 CameraPosition;
float3 SunLightDirection;
float4 SunLightColor;
float SunLightIntensity;

#define MAXLIGHT 20

float3 PointLightPosition[MAXLIGHT];
float4 PointLightColor[MAXLIGHT];
float PointLightIntensity[MAXLIGHT];
float PointLightRadius[MAXLIGHT];
int MaxLightsRendered = 0;

//////////////////////////////////////////////////////////////

Texture2D DiffuseTexture;
SamplerState textureSampler
{
    MinFilter = linear;
    MagFilter = Anisotropic;
    AddressU = Wrap;
    AddressV = Wrap;
};

///////////////////////////////////////

//The inputs we need from the model
struct VertexShaderInput
{
    float4 Position : SV_POSITION0;
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
};

struct VertexShaderOutput
{
    float4 Position : SV_POSITION0;
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
    float3 WorldPos : TEXCOORD2;
}

////////////////////////////////////////////////////////////////


//  Our Vertex Shader
VertexShaderOutput VertexShader(VertexShaderInput input)
{
    VertexShaderOutput Output;
    
    //Calculate the position on screen
    Output.Position = mul(input.Position, WorldViewProj);

    //Transform the normal to world space
    Output.Normal = mul(float4(input.Normal, 0), World).xyz;

    //UV coordinates for our textures
    Output.TexCoord = input.TexCoord;

    //The position of our vertex in world space
    Output.WorldPos = mul(input.Position, World).xyz;
    
    return Output;
}

// Our lighting equations

float4 CalcDiffuseLight(float3 normal, float3 lightDirection, float4 lightColor, float lightIntensity)
{
    return saturate(dot(normal, -lightDirection)) * lightIntensity * lightColor;
}

float4 CalcSpecularLight(float3 normal, float3 lightDirection, float3 cameraDirection, float4 lightColor, float lightIntensity)
{
    float3 halfVector = normalize(lightDirection + cameraDirection);
    float specular = saturate(dot(halfVector, normal));

    //I have all models be the same reflectance
    float specularPower = 2;

    return lightIntensity * lightColor * pow(abs(specular), specularPower);
}

// The squared length of a vector
float lengthSquared(float3 v1)
{
    return v1.x * v1.x + v1.y * v1.y + v1.z * v1.z;
}

//  Our pixel Shader

float4 PixelShader(VertexShaderOutput input)
{
    float4 baseColor = DiffuseTexture.Sample(textureSampler, input.TexCoord);

    float4 diffuseLight = float4(0, 0, 0, 0);
    float4 specularLight = float4(0, 0, 0, 0);

    //calculate our viewDirection
    float3 cameraDirection = normalize(input.WorldPos - CameraPosition);

    //calculate our sunlight
    diffuseLight += CalcDiffuseLight(input.Normal, SunLightDirection, SunLightColor, SunLightIntensity);
    diffuseSpecular += CalcSpecularLight(input.Normal, SunLightDirection, cameraDirection, SunLightColor, SunLightIntensity);

    //calculate our pointLights
    [loop]
    for (int i = 0; i < MaxLightsRendered; i++)
    {
        float3 PointLightDirection = input.WorldPos - PointLightPosition[i];
                           
        float DistanceSq = lengthSquared(PointLightDirection );



        float radius = PointLightRadius[i];
             
        [branch]
        if (DistanceSq < abs(radius * radius))
        {
            float Distance = sqrt(DistanceSq);

            PointLightDirection /= Distance;

            float du = Distance / (1 - DistanceSq / (radius * radius - 1));

            float denom = du / abs(radius) + 1;

            //The attenuation is the falloff of the light depending on distance basically
            float attenuation = 1 / (denom * denom);

            diffuseLight += CalcDiffuseLight(input.Normal, PointLightDirection, PointLightColor[i], PointLightIntensity[i]) * attenuation; 
            
            specularLight += CalcSpecularLight(input.Normal, PointLightDirection, cameraDirection, PointLightColor[i], PointLightIntensity[i]) * attenuation;
        }
    }

    return diffuseLight * baseColor + diffuseSpecular;
}

technique BasicLightShader
{
    pass Pass1
    {
        VertexShader = compile vs_5_0 VertexShader();
        PixelShader = compile ps_5_0 PixelShader();
    }
}

Try it out and report back!

Note: In monogame you can add your shaders to your content pipeline and then load them like this
LightingEffect = content.Load(“FX/LightShader”);

Later for the models you can use them like this

LightingEffect.CurrentTechnique.Passes[0].Apply();

Before calling MyModel.Draw()

You need to supply the shader with info like CameraPosition, SunLightColor etc. every time these change.

You can do this like this

LightingEffect.Parameters[“SunLightColor”].SetValue( _mySunLightColor);

alternatively you can store/use these parameter pointers like this

Initialize()
LightingEffectWorldViewProj = LightingEffect.Parameters[“WorldViewProj”];

Draw()
LightingEffectWorldViewProj.SetValue(World * View * Projection);

The point lights I use like this:

Initialize()
{
_lightingEffectPointLightPosition = lightingEffect.Parameters[“PointLightPosition”];
_lightingEffectPointLightColor = lightingEffect.Parameters[“PointLightColor”];
_lightingEffectPointLightIntensity = lightingEffect.Parameters[“PointLightIntensity”];

        _lightingEffectPointLightRadius = lightingEffect.Parameters["PointLightRadius"];
        _lightingEffectMaxLightsRendered = lightingEffect.Parameters["MaxLightsRendered"];

}

Draw()
{
_lightingEffectMaxLightsRendered.SetValue( lightsRendered);

            _lightingEffectPointLightPosition.SetValue(PointLightPosition);
            _lightingEffectPointLightColor.SetValue(PointLightColor);
            _lightingEffectPointLightIntensity.SetValue(PointLightIntensity);
            _lightingEffectPointLightRadius.SetValue(PointLightRadius);

}

For each point light you need to store :
Position (Vector3)
Color (Vector4 / Color)
Intensity (float)
Radius (float)

And save these value in an array like this
Vector3[] PointLightPosition = new Vector3[MaxLightsGpu];

to be able to pass it onto the gpu like this

_lightingEffectPointLightPosition.SetValue(PointLightPosition);

I hope this basic set up helped you.

Hey there kosmonaut!

Sorry for the long delay. My work day is just ending and I’m finally looking at this again. I utilize texture + normal map. I haven’t played with any PBR configurations as of yet. I saw that you linked a basic point light shader below, I will give that a try. I did discover a bug in my first round of playing with ambient light shaders. The reason my models were adopting the coloring was because I wasn’t taking into account the texture on my model at all. I was purely doing a color modification only. I am going to play with your shader for sure, but I feel like I must keep learning how these things work! Thank you for all your help. I hope to report back as soon as I solve this with some results.

Hey Jjagg,

Thank you for the resources. I’m going to add this to my reading list so that I can better understand shaders. I appreciate you taking the time to respond. It seems I have much to learn.

So far so good. I found a few minor errors which I correct, but I ran into one thing that I couldn’t quite anticipate on my own. For this code:

 float3 PointLightDirection = input.WorldPos - PointLightPosition[i];
                           
 float DistanceSq = lengthSquared(DirectionToLight);

DirectionToLight seems to be not declared, I suspect you meant to use the above variable PointLightDirection here, but I’m not sure. Is that correct?

I believe I am close! So far, this is the state of the shader you provided. I made a few adjustments which I will detail after my paste of the code.

// our inputs per model
matrix World;
matrix WorldViewProj;       //This is world * view * projection

// our inputs per change in lighting / draw

float3 CameraPosition;
float3 SunLightDirection;
float4 SunLightColor;
float SunLightIntensity;

#define MAXLIGHT 20

float3 PointLightPosition[MAXLIGHT];
float4 PointLightColor[MAXLIGHT];
float PointLightIntensity[MAXLIGHT];
float PointLightRadius[MAXLIGHT];
int MaxLightsRendered = 0;

//////////////////////////////////////////////////////////////

Texture2D DiffuseTexture;
SamplerState textureSampler
{
    MinFilter = linear;
    MagFilter = Anisotropic;
    AddressU = Wrap;
    AddressV = Wrap;
};

///////////////////////////////////////

//The inputs we need from the model
struct VertexShaderInput
{
    float4 Position : SV_POSITION0;
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
};

struct VertexShaderOutput
{
    float4 Position : SV_POSITION0;
    float3 Normal : NORMAL0;
    float2 TexCoord : TEXCOORD0;
    float3 WorldPos : TEXCOORD2;
};

////////////////////////////////////////////////////////////////


//  Our Vertex Shader
VertexShaderOutput MainVS(VertexShaderInput input)
{
    VertexShaderOutput Output;
    
    //Calculate the position on screen
    Output.Position = mul(input.Position, WorldViewProj);

    //Transform the normal to world space
    Output.Normal = mul(float4(input.Normal, 0), World).xyz;

    //UV coordinates for our textures
    Output.TexCoord = input.TexCoord;

    //The position of our vertex in world space
    Output.WorldPos = mul(input.Position, World).xyz;
    
    return Output;
}

// Our lighting equations

float4 CalcDiffuseLight(float3 normal, float3 lightDirection, float4 lightColor, float lightIntensity)
{
    return saturate(dot(normal, -lightDirection)) * lightIntensity * lightColor;
}

float4 CalcSpecularLight(float3 normal, float3 lightDirection, float3 cameraDirection, float4 lightColor, float lightIntensity)
{
    float3 halfVector = normalize(lightDirection + cameraDirection);
    float specular = saturate(dot(halfVector, normal));

    //I have all models be the same reflectance
    float specularPower = 2;

    return lightIntensity * lightColor * pow(abs(specular), specularPower);
}

// The squared length of a vector
float lengthSquared(float3 v1)
{
    return v1.x * v1.x + v1.y * v1.y + v1.z * v1.z;
}

//  Our pixel Shader

float4 PS(VertexShaderOutput input) : COLOR0
{
    float4 baseColor = DiffuseTexture.Sample(textureSampler, input.TexCoord);

    float4 diffuseLight = float4(0, 0, 0, 0);
    float4 specularLight = float4(0, 0, 0, 0);
    float4 diffuseSpecular = float4(0, 0, 0, 0);

    //calculate our viewDirection
    float3 cameraDirection = normalize(input.WorldPos - CameraPosition);

    //calculate our sunlight
    diffuseLight += CalcDiffuseLight(input.Normal, SunLightDirection, SunLightColor, SunLightIntensity);
    diffuseSpecular += CalcSpecularLight(input.Normal, SunLightDirection, cameraDirection, SunLightColor, SunLightIntensity);

    //calculate our pointLights
    [loop]
    for (int i = 0; i < MaxLightsRendered; i++)
    {
        float3 PointLightDirection = input.WorldPos - PointLightPosition[i];
                           
        float DistanceSq = lengthSquared(PointLightDirection);

        float radius = PointLightRadius[i];
             
        [branch]
        if (DistanceSq < abs(radius * radius))
        {
            float Distance = sqrt(DistanceSq);

            PointLightDirection /= Distance;

            float du = Distance / (1 - DistanceSq / (radius * radius - 1));

            float denom = du / abs(radius) + 1;

            //The attenuation is the falloff of the light depending on distance basically
            float attenuation = 1 / (denom * denom);

            diffuseLight += CalcDiffuseLight(input.Normal, PointLightDirection, PointLightColor[i], PointLightIntensity[i]) * attenuation; 
            
            specularLight += CalcSpecularLight(input.Normal, PointLightDirection, cameraDirection, PointLightColor[i], PointLightIntensity[i]) * attenuation;
        }
    }

    return diffuseLight * baseColor + diffuseSpecular;
}

technique BasicLightShader
{
    pass Pass1
    {
        VertexShader = compile vs_4_0 MainVS();
        PixelShader = compile ps_4_0 PS();
    }
}
  • Added a semicolon after the VertexShaderOutput structure declaration.
  • Changed the VertexShader function name to MainVS (The content pipeline tool was complaining about “VertexShader” being an unexpected token.
  • Changed the PixelShader function name to PS for the same reason as above.
  • Changed the DirectionToLight argument in the lengthSquared call to PointLightDirection (See my earlier reply).
  • Changed the PS function to have a return type of : COLOR0 (Is this even the correct thing to do???). I did this because the Content Pipeline tool complained about missing return data for the PixelShader.

Right now, I am running with the pixel shader, but everything is solid black and white when I set the SunlightIntensity very high. It appears like ambient darkness when I set it low, but all of my models are still black and white.

To save you reading time, I’ll restate the parts about my model. My models are all FBX with textures and normal data. To assist you, I’ve uploaded one of the models to my dropbox. It should be accessible here.

For the sake of clearing up differences. I currently render my model using DrawIndexedPrimitives like so. Is this okay vs. model.Draw?

                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    GraphicsDevice.SetVertexBuffer(meshPart.VertexBuffer, meshPart.VertexOffset);
                    GraphicsDevice.Indices = meshPart.IndexBuffer;
                    _ambientEffect.CurrentTechnique.Passes[0].Apply();

                    GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, meshPart.StartIndex, meshPart.PrimitiveCount);
                }

Once again, thank you for all your help. Once I get this working, I’m going to get this all up on a tutorial page somewhere!

Holy crap, i wrote that thing deep in the night yesterday and there are many errors, sorry :frowning:

foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    GraphicsDevice.SetVertexBuffer(meshPart.VertexBuffer, meshPart.VertexOffset);
                    GraphicsDevice.Indices = meshPart.IndexBuffer;
                    _ambientEffect.CurrentTechnique.Passes[0].Apply();

                    GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, meshPart.StartIndex, meshPart.PrimitiveCount);
                }

this is the way the normal model.draw() is done in monogame anyways I believe, so yes it is correct.


•Changed the PS function to have a return type of : COLOR0 (Is this even the correct thing to do???). I did this because the Content Pipeline tool complained about missing return data for the PixelShader.

yes, sorry.


float4 specularLight = float4(0, 0, 0, 0);
float4 diffuseSpecular = float4(0, 0, 0, 0);

these should have been the same, sorry I sometimes used different terms, it’s not diffuseSpecular.

Direction to Light is not the same as PointLightDirection, it’s the opposite. But that doesn’t matter in the context of calculating the length.

Everything is black and white you say? So on the one side white on the other black? Try changing the default specularity. I don’t work with these basics any more, try out values other than 2.

The light intensities should not be super high, values < 2/1 unless you use some tonemapping later on.

If you want to have physically based rendering you need a lot more calculations in your diffuse / spec function, but also a lot more inputs like roughness / metalness etc so I used the basic variant.


So here is the updated version

for (int i = 0; i < MaxLightsRendered; i++)
    {
        float3 PointLightDirection = input.WorldPos - PointLightPosition[i];
                           
        float DistanceSq = lengthSquared(PointLightDirection );

        float radius = PointLightRadius[i];
             
        [branch]
        if (DistanceSq < abs(radius * radius))
        {
            float Distance = sqrt(DistanceSq);

            //normalize
            PointLightDirection /= Distance;

            float du = Distance / (1 - DistanceSq / (radius * radius - 1));

            float denom = du / abs(radius) + 1;

            //The attenuation is the falloff of the light depending on distance basically
            float attenuation = 1 / (denom * denom);

            diffuseLight += CalcDiffuseLight(input.Normal, PointLightDirection, PointLightColor[i], PointLightIntensity[i]) * attenuation; 
            
            specularLight += CalcSpecularLight(input.Normal, PointLightDirection, cameraDirection, PointLightColor[i], PointLightIntensity[i]) * attenuation;
        }
    }

No worries about the errors, I’m very deeply appreciative of your help! If anything I find that debugging helps me learn. I will give your changes a try as soon as I get time tomorrow. I will try to tone down the specularity and see what that gets me. It’s fun to tweak the values for learning.

Thank you again for your help, I will report back as I try your tweaks.

This is where I’m at today. I changed the loop with your modifications. Regardless of whether or not I increase or decrease the specular value, or sunlight intensity, this is my result. I’m not too sure where to go from here. I’m going to keep playing with values to see what I can do.

Finally, success kosmo! I am very close now. I wasn’t setting my DiffuseTexture in the shader this whole time :frowning:

I ended up grabbing the texture from the current model and set it during draw. Don’t mind the terrible pixel art, just a placeholder. I’ve included two screenshots, one with a blue light and one lit somewhat normally. I’ve circled the bottom screenshot to highlight that for some reason, whatever I set sunlight to (In the below case, black), it colors whole surfaces that color. I’m still playing and tweaking, but I suspect this is something small.

If you have any insights on things I can tweak and try, I’d love to hear them. I cannot thank you enough for your help with all of this. I’ve learned so much! Now I have this feeling that I must keep going and learn more and more!

EDIT: I also noticed that lights will bleed through walls, but I suspect this is more due to intensity settings on my part.

Hey, that looks good!

light will bleed through walls and and all walls that look in the opposite of the sun light direction will get sunlight.

Light bleeds through walls because right now the pixels get colored depending on their normal (the direction they are facing) and their distance to the point lights. They have no clue that they are actually in shadow.

If you want to eliminate light bleeding you have to implement shadows! :stuck_out_tongue:

One thing though: If you do not want to write your own engine and learn 3d rendering on a basic level I would highly recommend switching to a proper engine that does that for you.
Shadows, lighting, material behaviour, animation etc. all would have to be implemented by yourself otherwise.
Personally I love this whole affair though, so I’ll stick with MonoGame right now.

I come from a more traditional coding background, -!; I love the challenges that 3D is giving me. I’m going to take the next step and learn how to implement shadows, hopefully I can get something work tonight. I too love doing things this way. The “experts” will tell you not to implement your own engine, but to me it is where the fun lies.

I’ll keep showIng oh my progress, more for fun than anything else! Once I get shadows in place, I’m going to put the shader out for free along with a tutorial.

great, I’m the same and 3d is super fun :slight_smile:

Actually I haven’t needed point light shadows yet, but I think I’ll try to do some myself today maybe

1 Like

Nice! I’d be curious to see what you come up with if you intend to share :slight_smile:

I’m going to try to do the same. I’m still trying to figure it out from there, but I’m hoping to make progress.

I struggle with how you apply the two shaders. It seems like shadow mapping only has the position of light, not any shading data, because you use the position to determine the normal data from the light position, I’m trying to understand how you overlay the shadow data around what’s lit or the other way around. Either way I’m playing with it until I can figure it out!

When @kosmonautgames says he intends to try something, it can end up in a full blog post detailing the entire process. It’s happened before. :wink:

2 Likes

ok guys, I’ve set up a new blog post about … nah not yet.

However, I build a new monogame project with the shader.

I’ve noticed I made another mistake in the specular shader, where i mistakenly forgot a minus

here is a better version

float4 CalcSpecularLight(float3 normal, float3 lightDirection, float3 cameraDirection, float4 lightColor, float lightIntensity)
{
    float3 halfVector = normalize(-lightDirection + -cameraDirection);
    float specular = saturate(dot(halfVector, normal));

    //I have all models be the same reflectance
    float specularPower = 20;

    return lightIntensity * lightColor * pow(abs(specular), specularPower);
}

Another note: I set it up so you can easily replace the diffuse and specular with more sophisticated shaders, like a diffuse Oren-Nayar model and a specular Cook-Torrance.
These are more expensive but lights look more realistic. Not sure if you need that though, just a note if you think the lighting is lacking.

Point light shadows are a huge pain actually, but I’ve got them working somehow in the most unefficient way possible.

3 Likes