Xor Shaders
  • Tutorials
  • Contact
  • About

#7 Introduction to 3D part 2

6/12/2015

17 Comments

 
In the last tutorial we made a basic flood shader. Today we'll make a basic 3D lighting shader.
Lighting shader usually use normals. As you may remember from the last tutorial, normals are the vectors which are perpendicular to each triangle. I will explain them in better detail.

Normals

Normal vector information can be received using the "in_Normal" attribute. This is only processed for each vertex (because it's in the vertex shader). So we'll create a varying vector called "v_vNormal" and we'll add it to both shaders (fragment and vertex). This way the normal can processed per pixel rather than per vertex. To test this shader we'll set the color to the normals so we can see what the normals look like. This is the code I used:

gl_FragColor = vec4(v_vNormal*0.5+0.5,1.0);

Because normals are in the range of -1 and 1 we'll multiply it be 0.5 and add 0.5 to put it in the range of 0 to 1 (since we can't see negative colors). 

Here is what it looks like (downloads at the bottom):
Picture
That is a simple scene with a sphere, cube and cone. You can see the normals with our shader. When you make your own models, you'll want to make sure the normals are correct.
Now we'll move on to a simple lighting shader.

Simple lighting

Now that we get normals we'll make some simple directional lighting. To do this we will need to create a few vectors. We'll create a vec3 called "dir" and set it to something like "vec3( 0.6, 0.1, -1.0)".
This will be the direction of our light. Next create another vec3 called "col". This will be for the light color. We'll set this to a nice blue color (I used "vec3( 0.5, 0.8, 1.0)").
Now create a float called "lighting". Since this will be for the brightness of the pixel we'll need to calculate how much light it is receiving. We'll use the dot product function. This function finds the difference between two vectors in the range of -1 to 1 (if used properly). The vectors should be normalized. When you normalize a vector it sets the length of the vector to 1 while keeping it in proportion. If you do not do this to the vectors of a dot product, then it will return something in a range larger (or smaller) than -1 to 1 and in this case, ruin the lighting.


So now we'll set lighting to the dot product of "v_vNormal" and "dir" (make sure their both normalized). This will find the difference of the normal and the light (between -1 and 1).
To convert this to a visible value we multiply it by 0.5 and add 0.5. Now mulitply the "lighting" float  and "col" vector to the texture color (making sure it is a vec4). Your code should look like this:

    vec3 dir = vec3( 0.6, 0.1, -1.0);//Light direction
    vec3 col = vec3( 0.5, 0.8, 1.0);//Light color
    float lighting = (dot( normalize(v_vNormal), normalize(dir)) * 0.5 + 0.5);//Calculate
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord) * vec4( lighting * col, 1.0);


This gets the desired light direction and color, then calculates it with "lighting" and finally applies it to the texture. This is the result (downloads below):
Picture
As you can see, our nice scene now has lighting. Lets take this farther by creating a point light.

Point lights

Now that you have a basic understanding of 3D lighting we can create point lights.
We want the point lights to have a radius. So we can add a float called "rad".
Since these lights cast shine in all directions, we'll need to change the "dir" vector.
This is actually simple to set, but we'll need the position of each pixel, so we'll add another varying vec3 and call it "v_vPosition". In the vertex shader, set it to "in_Position".

With the position we can calculate the direction from the light to each pixel. We simple put in the desired position for the point light (I used "vec3( 0.0, 32.0, -96.0)") and then subtract the pixel position ("v_vPoisiton"). That gets the direction for the light.

Now the last thing we'll add is a radius. This will make the light fade the farther the it is from the surface of each object. This is called Attenuation so we'll make a variable called "attenuation". Set this variable to the length of "dir" (so we get the distance from the light) and divide it by "rad" (so the  brightness equals one when it is at the radius distance and 0 when at the light). This is opposite of what we want so we'll have it 1 minus are current code. To keep this from going negative we'll put the max function around our code with the second argument to 0 (which keeps it from going below 0).


The fragment code should look like this:

    vec3 dir = vec3( 0.0, 32.0, -96.0) - v_vPosition;//Light direction
    float rad = 128.0;//Light radius
    vec3 col = vec3( 0.5, 0.8, 1.0);//Light color
    float attenuation = max( 1.0 - length(dir)/rad, 0.0);//Calculate brightness
    float lighting = (dot( normalize(v_vNormal), normalize(dir)) * 0.5 + 0.5) * attenuation;//Calculate lighting
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord) * vec4( lighting * col, 1.0);



When running the code, the result should look like this (downloads at the bottom):
Picture
There's a simple light in the center of the screen which properly fades. You can easily add a uniform for changing light position, radius, direction, or color if you want, but that is up to the developer.

DOWNLOAD NORMALS EXAMPLE
DOWNLOAD DIRECTIONAL LIGHT EXAMPLE
DOWNLOAD POINT LIGHT EXAMPLE
17 Comments
BenSeawalker link
6/20/2015 04:09:26 pm

Excellent tutorials! :D
I'm knowledgeable in GML, C++, C#, Java, Javascript, and PHP, but shaders always seemed like black magic to me. haha

My only question would be how you would calculate multiple lights? Is there a way to pass the values of each light to the shader?
Or would you draw the scene to a surface, then run the shader on the surface for each light?

Reply
Xor
6/21/2015 07:45:24 am

Thanks!

I may do an array tutorial in the future, but I don't have one now.
I do however have a simple example of a project I started a while back:
http://gmc.yoyogames.com/index.php?showtopic=651569

To do an array you simply put brackets with the size of the array behind a uniform: uniform float array[8];
Then in GameMaker:Studio use "shader_set_uniform_f_array".

Reply
Nahuel3d
5/23/2016 10:15:06 am

Thanks a lot!
How can i add a normal map to the model??

Reply
Xor
5/23/2016 07:06:47 pm

Unfortunately that isn't simple at all. In order to calculate the normal map in world space you need the normal's bitangent. This will have to be calculated in HLSL 9 using ddx() and ddy() because the functions aren't available in GLSL.
I'm not planning on writing a tutorial on this anytime soon because I haven't even gotten together a working example yet.
Sorry I didn't make a tutorial yet.

Reply
Matthew
6/26/2016 07:01:09 am

And what about spot lights?

Reply
Xor link
6/27/2016 03:16:20 pm

Spot lights can be made using a point lights that have a direction component. Calculate it's attenuation like this:
max( 1.0 - length(dir)/rad, 0.0)* smoothstep(0.0,spot.w,dot(dir,spot.xyz));
Then you can set the vector spot as a vec4 with 3D direction and an arc between 0 (0 degrees) and 1 (90 degrees). Example:
vec4 spot = vec4(normalize(vec3( 0.6, 0.1, -1.0)),0.5);
I hope this helps. Thanks for visiting.

Reply
Matthew
7/7/2016 02:36:02 pm

I don´t know if I am doing something wrong with the spotlight, but It looks weird...
My code:

void main(){
vec3 dir = vec3( 16.0, 12.0, -9.0) - v_vPosition;
vec4 spot = vec4(normalize(vec3( 3.0, -0.7, 1.2)),0.5);
float rad = 256.0;//Light radius
vec3 col = vec3( 1.0, 1.0, 1.0);//Light color
float attenuation = max( 1.0 - length(dir)/rad, 0.0)*1.0* smoothstep(0.0,spot.w,dot(dir,spot.xyz));
float lighting = (dot( normalize(v_vNormal), normalize(dir)) * 0.5 + 0.5) * attenuation;//Calculate lighting
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord) * vec4( lighting * col, 1.0);
}

Reply
Xor link
7/8/2016 02:06:31 pm

My bad (I wasn't able to test it). Normalize dir, and calculate the spot light attenuation like this:

float attenuation = smoothstep(spot.w,1.0,dot(normalize(dir),spot.xyz));

This code gave nice results:
void main()
{
vec3 dir = vec3( 16.0, 12.0, -9.0) - v_vPosition;//Light to pixel direction
vec4 spot = vec4(normalize(vec3( 3.0, -0.7, 1.2)),0.5);//Spot light direction and size
float rad = 256.0;//Light radius
vec3 col = vec3( 1.0, 1.0, 1.0);//Light color
float attenuation = max( 1.0 - length(dir)/rad, 0.0)*smoothstep(spot.w,1.0,dot(spot.xyz,normalize(dir)));//Brightness
float lighting = (dot( normalize(v_vNormal), normalize(dir)) * 0.5 + 0.5) * attenuation;//Calculate total brightness
gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord) * vec4( lighting * col, 1.0);
}

Reply
JuJack
10/13/2016 09:48:03 am

Is it possible to add it to this shader?
https://www.youtube.com/watch?v=PoTrpLV-MaY

Then people could use it in all situations, like flashlights in horror games, direction lights for stuff like sun and point lights can be used like normal lamps.

Xor link
10/13/2016 03:43:27 pm

Well I don't have the time to put together an example, but that shader's code is here : https://www.dropbox.com/s/ekgbhpvjhlmc3zm/3DLightingShader.gmz?dl=0 and you can add the spotlight part using the same basic idea.

Reply
JuJack
10/15/2016 02:22:40 pm

I tried, but then I found a problem in the spotlight shader. It works wrong, with vertexes that have bigger angle than 90 degrees (Or something like that, I am not sure, but when I tested it on 3D landscape it didn´t work correctly)

Reply
Xor link
10/15/2016 04:36:10 pm

Okay. If you don't mind, you could send me an email with the source code and I'll have a look.

JuJack
10/16/2016 08:33:57 am

Part of problem solved! Sorry for previous comment, shader is working almost fine. Trouble was probably in normals. But another problem summoned. I am create forest, and I use h-map to make hills. Trees are normal models converted to GM:S code, like this:
tree_model = d3d_model_create();..................
When I use spotlight shader, hills and landscape are normaly visible, but trees are totaly black, even when spotlight is next to them... I can send u .gmz, but I do not know to which e-mail :)...

Reply
Xor link
10/16/2016 10:26:43 am

This sounds like you don't have any normals on the model. If you are generating the model in code, you need to include d3d_model_vertex_normal. My email is xorshaders@gmail.com if you need more info.

Reply
JuJack
10/16/2016 10:30:08 am

Like this?
d3d_model_primitive_begin(tree_model,4.0000);
d3d_model_vertex_normal_texture_color(tree_model,3.0000,-2.0000,0.0000,0.9465,0.3155,0.0676,0.1873,0.0625,16777215.0000,1.0000);
d3d_model_vertex_normal_texture_color(tree_model,2.0000,1.0000,0.0000,0.9465,0.3155,0.0676,0.0200,0.0625,16777215.0000,1.0000);

Xor link
10/16/2016 12:24:37 pm

That should work. Send me the example. It must the shader code.

Ana
6/29/2017 06:53:00 am

Exactly what I was looking for. Thank you so much for these amazing tutorials! Very helpful and easy to understand.

Reply



Leave a Reply.

    Tutorials

    I am Xor and I make shader tutorials for GameMaker: Studio users. Follow me on twitter for updates!

    Categories

    All
    3D
    Beginner
    Intermediate

    Archives

    January 2019
    May 2016
    August 2015
    June 2015
    March 2015
    February 2015
    January 2015
    December 2014

    RSS Feed