如何把一个标准 GLSL 例程改写为 Codea shader
概述
这里所说的标准 GLSL
例程指的是 OpenGL ES 2.0/3.0
中使用 GLSL
编写, 由 顶点着色器
和 片段着色器
组成的 shader
. 在 <OpegGL ES 3.0 编程指南>
中有很多优秀的 shader
例程, 它们一般通过 C 程序
来调用, 我们现在希望把它们改写为 Codea shader
, 也就是准备用 Lua
来调用, 以一个 法线贴图
的例程为例说明改写过程细节.
法线贴图例程
顶点着色器代码
下面是顶点着色器原始代码:
uniform mat4 u_matViewInverse;
uniform mat4 u_matViewProjection;
uniform vec3 u_lightPosition;
uniform vec3 u_eyePosition;
varying vec2 v_texcoord
varying vec3 v_viewDirection
varying vec3 v_lightDirection
attribute vec4 a_vertex;
attribute vec2 a_texcoord0;
attribute vec3 a_normal;
attribute vec3 a_binormal;
attribute vec3 a_tangent;
void main(void) {
// Transform eye vector into world space
vec3 eyePositionWorld =
(u_matViewInverse * vec4(u_eyePosition, 1.0)).xyz;
// Compute world space direction vector
vec3 viewDirectionWorld = eyePositionWorld - a_vertex.xyz;
// Transform light position into world space vec3 lightPositionWorld =
(u_matViewInverse * vec4(u_lightPosition, 1.0)).xyz;
// Compute world space light direction vector
vec3 lightDirectionWorld = lightPositionWorld - a_vertex.xyz;
// Create the tangent matrix
mat3 tangentMat = mat3(a_tangent,
a_binormal,
a_normal);
// Transform the view and light vectors into tangent space
v_viewDirection = viewDirectionWorld * tangentMat;
v_lightDirection = lightDirectionWorld * tangentMat;
// Transform output position
gl_Position = u_matViewProjection * a_vertex;
// Pass through texture coordinate
v_texcoord = a_texcoord0.xy;
}
我们按照代码顺序来一行行地进行转换, 首先变量声明, 有 4
个统一变量 uniform
, 有 3
传给片段着色器的变量 varying
, 有 5
个来自主程序的顶点属性 attribute
.
我们需要处理的就是找到 uniform
和 attribute
在 Codea
中的对应设置, 依次进行:
统一变量的对应设置
4个统一变量如下:
uniform mat4 u_matViewInverse;
uniform mat4 u_matViewProjection;
uniform vec3 u_lightPosition;
uniform vec3 u_eyePosition;
按照变量名的含义:
u_matViewInverse
应该是viewMatrix
的逆矩阵;u_matViewProjection
应该是viewMatrix
和projectionMatrix
的相乘;u_lightPosition
应该是光源位置, 由我们自行设置;u_eyePosition
应该是摄像机位置, 可以通过camera
来设置.
视图矩阵的逆矩阵, 在书中描述是为了把 u_lightPosition
和 u_eyePosition
从视图空间转换到世界空间, 那么视图矩阵就是从世界空间转换到视图空间了?
属性的对应设置
在 Codea
中, 有一些约定俗成的设置, 比如顶点的属性 attribute
, 就有一些预先定义好的:
position
顶点坐标texCoord
顶点的纹理坐标color
顶点的颜色normal
顶点的法线
在上面的顶点着色器代码中, 我们看到用了这么几个属性:
attribute vec4 a_vertex;
attribute vec2 a_texcoord0;
attribute vec3 a_normal;
attribute vec3 a_binormal;
attribute vec3 a_tangent;
很明显, 对应关系如下:
a_vertex
跟position
一样;a_texcoord0
跟texCoord
一样;a_normal
跟normal
一样.
还有两个属性 a_binormal 次法线
和 a_tangent 切线
, 在 Codea
中没有对应的属性, 就需要我们自己计算了, 这部分可以在 Lua
主程序中计算, 也可以在 shader
中计算. 如果在 Lua
中计算, 那么它们的声明可以保持不变, 如果在 shader
中计算, 就不能声明为 attribute
了.
原本计划为减少调试难度, 我们决定先在 Lua
中计算, 确认调试通过了, 再改写为 shader
计算.
后来看到这篇教程 Mesh Deformers with the GLSL, 给出了在 shader
中根据 normal
属性计算 binormal
和 tangent
的算法, 所以我们就直接引用一下:
vec3 tangent;
vec3 binormal;
vec3 c1 = cross(gl_Normal, vec3(0.0, 0.0, 1.0));
vec3 c2 = cross(gl_Normal, vec3(0.0, 1.0, 0.0));
if(length(c1)>length(c2))
{
tangent = c1;
}
else
{
tangent = c2;
}
// 归一化切线
tangent = normalize(tangent);
binormal = cross(gl_Normal, tangent);
binormal = normalize(binormal);
所以, 最终得到的顶点着色器代码为:
uniform mat4 u_matViewInverse;
uniform mat4 u_matViewProjection;
uniform vec3 u_lightPosition;
uniform vec3 u_eyePosition;
varying vec2 v_texcoord
varying vec3 v_viewDirection
varying vec3 v_lightDirection
attribute vec4 a_vertex;
attribute vec2 a_texcoord0;
attribute vec3 a_normal;
// attribute vec3 a_binormal;
// attribute vec3 a_tangent;
void main(void) {
// 先根据 normal 计算 binormal 和 tangent
vec3 tangent;
vec3 binormal;
vec3 c1 = cross(normal, vec3(0.0, 0.0, 1.0));
vec3 c2 = cross(normal, vec3(0.0, 1.0, 0.0));
if(length(c1)>length(c2))
{
tangent = c1;
}
else
{
tangent = c2;
}
// 归一化切线和次法线
tangent = normalize(tangent);
binormal = cross(normal, tangent);
binormal = normalize(binormal);
// 坐标空间转换: 摄像机坐标 Transform eye vector into world space
vec3 eyePositionWorld =
(u_matViewInverse * vec4(u_eyePosition, 1.0)).xyz;
// Compute world space direction vector
vec3 viewDirectionWorld = eyePositionWorld - a_vertex.xyz;
// Transform light position into world space
vec3 lightPositionWorld =
(u_matViewInverse * vec4(u_lightPosition, 1.0)).xyz;
// Compute world space light direction vector
vec3 lightDirectionWorld = lightPositionWorld - a_vertex.xyz;
// Create the tangent matrix
mat3 tangentMat = mat3(a_tangent,
a_binormal,
a_normal);
// Transform the view and light vectors into tangent space
v_viewDirection = viewDirectionWorld * tangentMat;
v_lightDirection = lightDirectionWorld * tangentMat;
// Transform output position
gl_Position = u_matViewProjection * a_vertex;
// Pass through texture coordinate
v_texcoord = a_texcoord0.xy;
}
片段着色器代码
下面是片段着色器原始代码:
precision mediump float;
uniform vec4 u_ambient;
uniform vec4 u_specular;
uniform vec4 u_diffuse;
uniform float u_specularPower;
uniform sampler2D s_baseMap;
uniform sampler2D s_bumpMap;
varying vec2 v_texcoord;
varying vec3 v_viewDirection;
varying vec3 v_lightDirection;
void main(void) {
// Fetch basemap color
vec4 baseColor = texture2D(s_baseMap, v_texcoord);
// Fetch the tangent-space normal from normal map
vec3 normal = texture2D(s_bumpMap, v_texcoord).xyz;
// Scale and bias from [0, 1] to [-1, 1] and normalize
normal = normalize(normal * 2.0 - 1.0);
// Normalize the light direction and view direction
vec3 lightDirection = normalize(v_lightDirection);
vec3 viewDirection = normalize(v_viewDirection);
// Compute N.L
float nDotL = dot(normal, lightDirection);
// Compute reflection vector
vec3 reflection = (2.0 * normal * nDotL) - lightDirection;
// Compute R.V
float rDotV = max(0.0, dot(reflection, viewDirection));
// Compute Ambient term
vec4 ambient = u_ambient * baseColor;
// Compute Diffuse term
vec4 diffuse = u_diffuse * nDotL * baseColor;
// Compute Specular term
vec4 specular = u_specular * pow(rDotV, u_specularPower);
// Output final color
gl_FragColor = ambient + diffuse + specular;
}
这段代码不需要任何修改, 直接使用就行
Lua 主程序
我们用来加载 shader
的 Lua
主程序如下:
function setup()
print("normal 3D")
tchx=0
tchy=0
createMesh()
cam = vec3(0, 0, 1000)
obj = vec3(0, 0, 0)
light = vec3(tchx, tchy, 0.075)
end
function draw()
perspective(50, WIDTH/HEIGHT)
light = vec3(tchx, tchy, 0.0075)
setShaderParam()
camera(cam.x, cam.y, cam.z, obj.x, obj.y, obj.z)
m:draw()
end
function createMesh()
m = mesh()
local w,h = WIDTH,HEIGHT
local img1, img2 = readImage("Dropbox:n1"), readImage("Dropbox:n2")
m:addRect(w/2,h/2,w,h)
-- m:addRect(0,0,w,h)
---[[
m.shader = shader(shader2.vs,shader2.fs)
m.shader.u_matViewInverse = viewMatrix():inverse()
m.shader.u_matViewProjection = viewMatrix() * projectionMatrix()
m.shader.s_baseMap = img1
m.shader.s_bumpMap = img2
--]]
--[[
m.shader.u_lightPosition = light or vec3(100, 100, 100)
m.shader.u_eyePosition = cam or vec3(100, 100, 100)
-- ambient/specular/diffuse 环境光,反射光,散射
m.shader.u_ambient = vec4(0.15, 0.15, 0.15, 0.8)
m.shader.u_specular = vec4(0.05, 0.05, 0.05, 0.8)
m.shader.u_diffuse = vec4(0.105, 0.005, 0.005, 0.8)
m.shader.u_specularPower = 0.09
--]]
-- m.texture = img1
end
function setShaderParam()
m.shader.u_lightPosition = light or vec3(100, 100, 100)
m.shader.u_eyePosition = cam or vec3(100, 100, 100)
-- ambient/specular/diffuse 环境光,反射光,散射
m.shader.u_ambient = vec4(0.37,0.37,0.37,1.0)
m.shader.u_specular = vec4(0.5,0.5,0.5,1.0)
m.shader.u_diffuse = vec4(0.88,0.88,0.88,1.0)
m.shader.u_specularPower = .05
end
function touched(touch)
if touch.state == BEGAN or touch.state == MOVING then
tchx=touch.x+50
tchy=touch.y+50
end
end
问题
1 无法改变距离
发现无法通过设置 camera
函数的参数来改变视角, 找了半天, 发现是顶点着色器中这条语句的原因;
gl_Position = u_matViewProjection * a_vertex;
它把视图投影矩阵应用于顶点, 变换后得到视图投影, 而不是常用的模型视图投影, 可以将其修改为:
uniform mat4 modelViewProjection;
gl_Position = modelViewProjection * a_vertex;
这样就可以调节摄像机和物体之间的距离了.
2 上下坐标错位
具体来说就是点击上方, 本来应该把光源放在上方, 结果下方出现高光, 说明上下坐标错位, 在我们的屏幕上也就是 y
轴坐标错位, 这一点可能是因为 Codea
使用了不同手系的坐标导致, 可以在顶点着色器中通过乘一个如下的矩阵来调整:
mat4 mm = mat4( 1.0, 0.0, 0.0, 0.0,
0.0, -1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0.
0.0, 0.0, 0.0, 1.0);
gl_Position = mm * modelViewProjection * a_vertex;
3 光线没有设置衰减
不设置衰减的光线会导致光源越远, 物体越亮的错误情况, 增加一个衰减系数就可以了