We need to know what are normals, vectors, normalize, dot product, etc. Because we are going to use for lighting.
Normals: these are unit vectors and also they must be perpendicular to the surface.
Normalize: It makes vector a unit vector, basically. We will use this method before dot product operation.
Dot product: The brightness is calculated with this formula.
for example the use of the dot product:
a: vec3(2, -1, 4)
b: vec3(3, 2, -1)
a.b = ?
a.b = 2.3 + (-1.2) + (4.-1)
= 6 - 2 - 4
= 0
Let's get into the programming part. Let's add new ObjectType called Light:
public enum ObjectType
{
Cube,
Plane,
Triangle2D,
Quad2D,
Light
}
Also, a new method called AddObject to ObjectManager as follows, (we did method overloading, this is just for easy usage, but don't remember to declare objectType field as public, also it can be changed as property):
public void AddObject(GameObject obj)
{
this.VAOManager.CreateVAO(obj.objectType);
this.ShaderManager.CreateShader(obj.objectType);
// GameObject gameObject = new GameObject(obj.objectType);
obj.VAO = this.VAOManager.VAOs[obj.objectType];
obj.EBO = this.VAOManager.EBOs[obj.objectType];
obj.IndexCount = this.VAOManager.IndexCounts[obj.objectType];
obj.Shader = this.ShaderManager.Shaders[obj.objectType];
this.gameObjects.Add(obj);
}
I will create a new mesh method for light. This method create a 3d cube for light object:
private void GenerateLight()
{
float len = 0.3f;
var (r,g,b,a) = (1.0f, 1.0f, 1.0f, 1.0f);
float[] vertices =
{
-len, -len, -len, r, g, b, a, // 0
len, -len, -len, r, g, b, a, // 1
len, -len, len, r, g, b, a, // 2
-len, -len, len, r, g, b, a, // 3
-len, len, -len, r, g, b, a, // 4
len, len, -len, r, g, b, a, // 5
len, len, len, r, g, b, a, // 6
-len, len, len, r, g, b, a // 7
};
int[] indices =
{
7, 6, 2, 7, 2, 3, // front face
4, 5, 1, 4, 1, 0, // back face
4, 5, 6, 4, 6, 7, // top face
0, 1, 2, 0, 2, 3, // bottom face
6, 5, 1, 6, 1, 2, // right face
7, 4, 0, 7, 0, 3 // left face
};
indexCount = 36;
GenerateVAOforLight(len, vertices, indices);
}
The GenerateVAOforLight as follows:
private void GenerateVAOforLight(float len, float[] vertices, int[] indices)
{
GL.GenVertexArrays(1, out vao);
GL.BindVertexArray(vao);
// create vbo to store the data in opengl and copy data to vbo
GL.GenBuffers(1, out vbo);
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
GL.BufferData(BufferTarget.ArrayBuffer, sizeof(float) * vertices.Length, vertices, BufferUsageHint.StaticDraw);
// create ebo
GL.GenBuffers(1, out ebo);
GL.BindBuffer(BufferTarget.ElementArrayBuffer, ebo);
GL.BufferData(BufferTarget.ElementArrayBuffer, sizeof(int) * indices.Length, indices, BufferUsageHint.StaticDraw);
// tell opengl how to use the data via attributes
// position attribute
// int attrPosition_Position = GL.GetAttribLocation(shader.ShaderProgram, "aPos");
int attrPosition_Position = 0;
GL.EnableVertexAttribArray(attrPosition_Position);
GL.VertexAttribPointer(attrPosition_Position, 3, VertexAttribPointerType.Float, false, sizeof(float) * 7, 0);
int attrPosition_Color = 1;
GL.EnableVertexAttribArray(attrPosition_Color);
GL.VertexAttribPointer(attrPosition_Color, 4, VertexAttribPointerType.Float, false, sizeof(float) * 7, sizeof(float) * 3);
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
GL.BindVertexArray(0);
Console.WriteLine("light vao: " + vao + " vbo: " + vbo + " ebo: " + ebo);
}
}
In the Mesh class constructor:
public Mesh(ObjectType objType)
{
if(objType is ObjectType.Quad2D) GenerateQuad();
if(objType is ObjectType.Cube) GenerateCube();
if(objType is ObjectType.Light) GenerateLight();
}
Also, we need to create a shader for light. I created a folder named light in shaders folder. I added the vertexShader and fragmentShader file like below:
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec4 aColor;
uniform mat4 uTransform;
uniform mat4 projection;
uniform mat4 view;
out vec4 passColor;
void main()
{
gl_Position = vec4(aPos, 1.0) * uTransform * view * projection;
passColor = aColor;
}
the fragment shader:
#version 330 core
in vec4 passColor;
out vec4 FragColor;
uniform vec4 lightColor;
void main()
{
FragColor = passColor * lightColor;
}
We will create the shader in ShaderManager class(We need to refactor this method but that's good for now. But if you want to refactor the code then go ahead.):
public void CreateShader(ObjectType objectType)
{
if(!Shaders.ContainsKey(objectType))
{
if(objectType == ObjectType.Quad2D)
{
Shader shader = new Shader("shaders/triangle/vertexShader.glsl","shaders/triangle/fragmentShader.glsl");
shaders[objectType] = shader;
}
if(objectType == ObjectType.Cube)
{
Shader shader = new Shader("shaders/cube/vertexShader.glsl","shaders/cube/fragmentShader.glsl");
shaders[objectType] = shader;
}
if(objectType == ObjectType.Light)
{
Shader shader = new Shader("shaders/light/vertexShader.glsl","shaders/light/fragmentShader.glsl");
shaders[objectType] = shader;
}
}
}
I'm going to create a Light class in objects folder. This class is going to inherit from the GameObject class. But we need to declare some methods of the GameObject class as virtual to override. The Light class is responsible for shaders of all objects that need to be illuminated.
namespace OpenTKTutorial
{
public class Light: GameObject
{
ShaderManager shaderManager;
public Vector4 LightColor {get; set;}
public Vector3 LightPosition {get; set;}
public Light(ShaderManager shaderManager, ObjectType objectType = ObjectType.Light) : base(objectType)
{
this.shaderManager = shaderManager;
LightColor = new Vector4(1f, 0, 0, 1f);
}
public override void Update()
{
Matrix4 Translation = OpenTK.Mathematics.Matrix4.CreateTranslation(this.Transform.Position);
Matrix4 Rotation = OpenTK.Mathematics.Matrix4.CreateRotationZ(MathHelper.DegreesToRadians(this.Transform.Rotation.Z));
Matrix4 Scale = OpenTK.Mathematics.Matrix4.CreateScale(this.Transform.Scale);
// rule : Translate x Rotation x Scale
Matrix4 Transform = Scale * Rotation * Translation;
Transformation = Transform;
foreach (var shader in shaderManager.Shaders)
{
if(shader.Value != this.Shader)
{
GL.UseProgram(shader.Value.ShaderProgram);
int uniformLocation_lightColor = GL.GetUniformLocation(shader.Value.ShaderProgram, "lightColor");
GL.Uniform4(uniformLocation_lightColor, LightColor);
int uniformLocation_lightPosition = GL.GetUniformLocation(shader.Value.ShaderProgram, "lightPosition");
GL.Uniform3(uniformLocation_lightPosition, this.Transform.Position);
}
}
}
public override void Draw()
{
GL.UseProgram(Shader!.ShaderProgram);
int uniformLocation_Transformation = GL.GetUniformLocation(Shader.ShaderProgram, "uTransform");
GL.UniformMatrix4(uniformLocation_Transformation, true, ref Transformation);
GL.BindVertexArray(VAO);
GL.DrawElements(PrimitiveType.Triangles, IndexCount, DrawElementsType.UnsignedInt, 0);
GL.BindVertexArray(0);
}
}
}
Let's visualize lighting as follows:
We need to normals from mesh. Normally, we don't need to calculate normals because when we will use Blender for models, this model will be contain normals already. Right now, we have no option like that. We need to define normals of the cube:
private void GenerateCube()
{
float len = 0.3f;
float[] vertices =
{
-len, -len, -len, // 0
len, -len, -len, // 1
len, -len, len, // 2
-len, -len, len, // 3
-len, len, -len, // 4
len, len, -len, // 5
len, len, len, // 6
-len, len, len, // 7
};
// 36 * 8 = 288
float[] cubeVertices = new float[288];
int[] indices =
{
7, 6, 2, 7, 2, 3, // front face
4, 5, 1, 4, 1, 0, // back face
4, 5, 6, 4, 6, 7, // top face
0, 1, 2, 0, 2, 3, // bottom face
6, 5, 1, 6, 1, 2, // right face
7, 4, 0, 7, 0, 3 // left face
};
float[] texCoords = { 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f };
float[] normals =
{
0f, 0f, 1f, // front
0f, 0f, -1f, // back
0f, 1f, 0f, // top
0f, -1f, 0f, // bottom
1f, 0f, 0f, // right
-1f, 0f, 0f // right
};
int nLine = 0;
for (int i = 0; i < indices.Length; i++)
{
if(i!=0 && i%6==0)
{
nLine++;
}
int index = indices[i];
cubeVertices[i * 8 + 0] = vertices[(index * 3) + 0];
cubeVertices[i * 8 + 1] = vertices[(index * 3) + 1];
cubeVertices[i * 8 + 2] = vertices[(index * 3) + 2];
cubeVertices[i * 8 + 3] = texCoords[(i % 6) * 2];
cubeVertices[i * 8 + 4] = texCoords[(i % 6) * 2 + 1];
cubeVertices[i * 8 + 5] = normals[(nLine % 6) * 3];
cubeVertices[i * 8 + 6] = normals[(nLine % 6) * 3 + 1];
cubeVertices[i * 8 + 7] = normals[(nLine % 6) * 3 + 2];
}
GenerateVAOforCube(len, cubeVertices, indices);
}
I added new attribute pointer for normals in GenerateVAOforCube:
private void GenerateVAOforCube(float len, float[] vertices, int[] indices)
{
...
int attrPosition_Position = 0;
GL.EnableVertexAttribArray(attrPosition_Position);
GL.VertexAttribPointer(attrPosition_Position, 3, VertexAttribPointerType.Float, false, sizeof(float) * 8, 0);
int attrPosition_TextureCoords = 1;
GL.EnableVertexAttribArray(attrPosition_TextureCoords);
GL.VertexAttribPointer(attrPosition_TextureCoords, 2, VertexAttribPointerType.Float, false, sizeof(float) * 8, sizeof(float) * 3);
int attrPosition_Normal = 2;
GL.EnableVertexAttribArray(attrPosition_Normal);
GL.VertexAttribPointer(attrPosition_Normal, 3, VertexAttribPointerType.Float, false, sizeof(float) * 8, sizeof(float) * 5);
...
}
Let's update the vertex shader of cube:
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;
layout(location = 2) in vec3 aNormal;
uniform mat4 uTransform;
uniform mat4 projection;
uniform mat4 view;
uniform vec3 lightPosition;
out vec2 passTexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0) * uTransform * view * projection;
passTexCoord = aTexCoord;
}
Now, we can start to calculating of lighting effect on shader. Let's move on the vertex shader:#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;
layout(location = 2) in vec3 aNormal;
uniform mat4 uTransform;
uniform mat4 projection;
uniform mat4 view;
uniform vec3 lightPosition;
out vec2 passTexCoord;
out vec3 passNormal;
out vec3 lightVector;
void main()
{
gl_Position = vec4(aPos, 1.0) * uTransform * view * projection;
passTexCoord = aTexCoord;
passNormal = (vec4(aNormal, 0.0) * uTransform).xyz;
lightVector = lightPosition - (vec4(aPos, 1.0) * uTransform).xyz;
}
After calculation of normal and lightVector, we will pass these vectors to the fragment shader:
#version 330 core
in vec2 passTexCoord;
in vec3 passNormal;
in vec3 lightVector;
out vec4 FragColor;
uniform sampler2D textureSampler;
uniform vec4 lightColor;
void main()
{
vec3 normalizedNormal = normalize(passNormal);
vec3 normalizedLightVector = normalize(lightVector);
float calcDot = dot(normalizedNormal, normalizedLightVector);
float brightness = max(calcDot, 0.0);
vec4 diffuse = brightness * lightColor;
FragColor = texture(textureSampler, passTexCoord) * diffuse;
}
The vectors are normalized and used dot production after normalizing. The brightness value is obtained with the smallest value being at least 0. The brightness and the color of the light are multiplied and diffuse is obtained. Finally we multiplied the diffuse with texture pixel, and we get the illuminated pixel value. One of the most important issues to be considered is the multiplication order. If the multiplication order is not as above, you will not get the desired result:
That's cool but it's too dark. Especially there faces that don't see light. Let's define an ambient variable in the fragment shader:
void main()
{
float ambient = 0.1;
vec3 normalizedNormal = normalize(passNormal);
vec3 normalizedLightVector = normalize(lightVector);
float calcDot = dot(normalizedNormal, normalizedLightVector);
float brightness = max(calcDot, 0.0);
vec4 diffuse = brightness * lightColor;
FragColor = texture(textureSampler, passTexCoord) * (diffuse + ambient);
}
The result with ambient value:
I think that's enough for this post. But we're not done with lighting yet.
No comments:
Post a Comment