Within a rendering, every mesh in the scene typically undergoes transformation by the model matrix, view matrix, and projection matrix.
The model matrix dictates the position, orientation, and relative scale of a mesh within the scene. It transforms the vertex positions of the mesh into world space.
https://i.sstatic.net/LG29O.png
The view matrix specifies the viewpoint's direction and position in relation to the scene. It transforms from world space to view (eye) space.
https://i.sstatic.net/FnOCk.png
It's worth noting that the model view matrix modelViewMatrix
combines the view matrix and model matrix. However, in some cases, the model matrix may be an identity matrix, making the modelViewMatrix
essentially equal to the view matrix. This assumption is made when model transformations are done by vectors orientation
and offset
, rather than a model matrix.
Calculating light can be done in either view space or world space.
If the light calculations are in view space, light positions and directions need to be transformed from world space to view space. This conversion is commonly done on the CPU before each frame, setting up light parameter uniforms with view space coordinates. Since the view position is (0, 0, 0) in view space, the view vector becomes the normalized and inverse vertex position (in view space).
https://i.sstatic.net/PcUn5.png
Performing light calculations in view space is feasible when the light direction and position are set in view space (refer to three.js - Light). First, the normal vector must be transformed to world space before converting it to view space. This process mirrors how vertex position transformations are handled. Start by adding the normal vector to the vertex position, then transform this combined position to world space. The normal vector in world space is derived from the difference between the calculated position and the vertex position in world space.
vec3 wNPosition = position + normal;
vec3 wNV = cross(orientation.xyz, wNPosition);
wNPosition = wNV * 2.0 * orientation.w + cross(orientation.xyz, wNV) * 2.0 + wNPosition;
vec3 wNormal = normalize( wNPosition - vPosition );
Given these assumptions, your shader code may resemble the following:
vec3 wPosition = position;
vec3 wV = cross(orientation.xyz, wPosition);
wPosition = offset + wV * 2.0 * orientation.w + cross(orientation.xyz, wV) * 2.0 + wPosition;
vec4 ecPosition = modelViewMatrix * vec4(wPosition, 1.0);
vUv = uv;
gl_Position = projectionMatrix * ecPosition;
// transform normal vector to world space
vec3 wNPosition = position + normal;
vec3 wNV = cross(orientation.xyz, wNPosition);
wNPosition = offset + wNV * 2.0 * orientation.w + cross(orientation.xyz, wNV) * 2.0 + wNPosition;
vec3 ecNormal = normalize(mat3(modelViewMatrix) * (wNPosition - wPosition));
// ambient light
vLightFactor = ambientLightColor;
// diffuse light
vec3 ecToLight = normalize(directionalLights[0].direction);
float NdotL = max(0.0, dot(ecNormal, ecToLight));
vLightFactor += NdotL * directionalLights[0].color;
For the addition of specular light, the procedure is as follows:
// specular light
vec3 ecReflectLight = reflect( ecFromLight, ecNormal );
vec3 ecViewDir = normalize(-ecPosition.xyz);
float VdotR = max(0.0, dot(ecViewDir, ecReflectLight));
float kSpecular = 4.0 * pow( VdotR, 0.3 * shininess ); // <--- set shininess parameter
vLightFactor += kSpecular * directionalLights[0].color;
Expanding on the answer: Per fragment lighting
While Gouraud shading handles light calculations in the vertex shader, Phong shading manages light calculations in the fragment shader.
(see also GLSL fixed function fragment program replacement)
Vertex shader:
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat3 normalMatrix;
attribute vec3 position;
attribute vec3 offset;
attribute vec3 normal;
attribute vec2 uv;
attribute vec4 orientation;
varying vec2 vUv;
varying vec3 ecPosition;
varying vec3 ecNormal;
void main()
{
vec3 wPosition = position;
vec3 wV = cross(orientation.xyz, wPosition);
pos = offset + wV * 2.0 * orientation.w + cross(orientation.xyz, wV) * 2.0 + wPosition;
vec4 vPos = modelViewMatrix * vec4(wPosition, 1.0);
ecPosition = vPos.xyz;
vUv = uv;
gl_Position = projectionMatrix * vPos;
// transform normal vector to world space
vec3 wNPosition = position + normal;
vec3 wNV = cross(orientation.xyz, wNPosition);
wNPosition = offset + wNV * 2.0 * orientation.w + cross(orientation.xyz, wNV) * 2.0 + wNPosition;
ecNormal = normalize(mat3(modelViewMatrix) * (wNPosition - wPosition));
}
Fragment shader:
precision highp float;
varying vec2 vUv;
varying vec3 ecPosition;
varying vec3 ecNormal;
uniform sampler2D map;
uniform mat4 modelViewMatrix;
struct DirectionalLight {
vec3 direction;
vec3 color;
int shadow;
float shadowBias;
float shadowRadius;
vec2 shadowMapSize;
};
uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];
uniform vec3 ambientLightColor;
void main()
{
// ambient light
float lightFactor = ambientLightColor;
// diffuse light
vec3 ecToLight = normalize(directionalLights[0].direction);
float NdotL = max(0.0, dot(ecNormal, ecToLight));
lightFactor += NdotL * directionalLights[0].color;
// specular light
vec3 ecReflectLight = reflect( ecFromLight, ecNormal );
vec3 ecViewDir = normalize(-ecPosition.xyz);
float VdotR = max(0.0, dot(ecViewDir, ecReflectLight));
float kSpecular = 4.0 * pow( VdotR, 0.3 * shininess ); // <--- set up shininess parameter
lightFactor += kSpecular * directionalLights[0].color;
gl_FragColor = texture2D(map, vUv) * vec4(vec3(lightFactor), 1.0);
}
Further references:
- Transform the modelMatrix
- How does this faking the light work on aerotwist?
- GLSL fixed function fragment program replacement