版权声明:本文为博主原创文章,博客地址:http://blog.youkuaiyun.com/qq_26821643,未经同意不得转载。
以下内容需要读者理解熟悉Opengl的矩阵运算以及掌握Shader编程基础。
RayMarching(光影投射)
什么是RayMarching? 简而言之,它是一种基于着色器语言的体渲染技术。它能够突破三角面片渲染规则,渲染出更多的表面细节,例如结合一些噪点函数和RayMarching技术可以创建“自然”,“无限”的高画质地貌奇观。
以上高质量的地貌渲染样例来自ShaderToy,一个创建和分享着色器代码的神奇网站。
技术概要
RayMarching用于片元着色器。在着色器代码中需要一个构建几何体的距离函数,对于每个像素,通常步骤如下:
-
通过 gl_FragCoord.xy 计算三维空间的视点起点和射线方向。
-
使用距离函数在射线方向上逐步逼近,直到距离小于设定的精度,获得逼近几何体表面的坐标
-
根据逼近坐标计算Normal。
-
计算光照模型和其他光影效果(诸如:镜面,阴影 会使用到步骤2),输出gl_FragColor。
关于距离函数和RayMarching过程的理论参考链接:理论详解。
现实问题
在ShaderToy上可以找到很多RayMarching 的样例。它们能够产生很多惊艳的渲染效果,但是比较局限的是ShaderToy都是全窗口渲染,在openGL概念下可看作成在一个填充窗口的图元上使用片元着色器,这就导致在一般的以面片模型为主的三维场景中很难应用。本文章的重点以GLSL为例构建一个在一般三维场景有较好的渲染效果的 RayMarching 增强方案 。为了表述之便,将其称之为 Depth-RayMarching。
实验环境
- 推荐使用 freeglut glew 搭建最简单的 C++ OpenGL实验项目 。
- 确保实验过程中使用独立显卡渲染
从一个球开始
- 计算射线方向: 使用 gl_ModelViewMatrixInverse(模型矩阵的逆),gl_ProjectionMatrixInverse(投影矩阵的逆) 这两个Build-in Uniform。由此计算出的所有空间数据都是基于本地坐标系的。(请务必理解本地,世界,相机坐标体系概念)。
#version 130
uniform vec2 viewport;//窗口分辨率
//坐标归一到[-1,1]
vec2 coord2uv(vec2 coord){
return 2.0*coord/viewport.xy-1.0;
}
//通过uv计算本地坐标系下的射线方向
vec3 uv2ray(vec2 uv){
vec4 camdir = gl_ProjectionMatrixInverse*vec4(uv,1,1);
camdir = camdir/camdir.w;//W归一后 得出相机坐标体系下的点
vec3 dir = mat3(gl_ModelViewMatrixInverse)*vec3(camdir);
return normalize(dir);
}
//本地坐标系下的视点坐标
vec3 eyePos(){
return vec3(gl_ModelViewMatrixInverse*vec4(0,0,0,1));
}
//计算本地坐标系下的灯光方向(平行光)
vec3 lightPos(){
return normalize(mat3(gl_ModelViewMatrixInverse)*gl_LightSource[0].position.xyz);
}
//本地坐标系下点光源坐标
vec3 spotLightPos(){
return vec3(gl_ModelViewMatrixInverse*gl_LightSource[0].position);
}
- 距离逼近计算(Ray Marching) :在常规的算法过程的基础上添加片段丢弃和深度计算
#define MAXSTEP 40 //最大逼近步数
#define TOLERANCE 0.0001 // 距离需要减小到该值以下
float sdsphere(vec3 pos,float r){
return length(pos)-r;
}
vec2 map(vec3 pos){
return vec2(sdsphere(pos,1),0.5);
}
//输入:视点坐标,射线方向;
//输出:最近点和对应材质系数。
bool marching(vec3 pos,vec3 ray,out vec3 closeing,out float material){
int step =0;
float mindist = 1;
vec2 res;
do{
res = map(pos);
if(res.x<mindist){
mindist = res.x;
material = res.y;
closeing = pos;
}
pos+= ray*res.x;
step++;
}while(res.x>TOLERANCE&&step<=MAXSTEP);
return step<=MAXSTEP;
}
float procDepth(vec3 localPos){
vec4 frag = gl_ModelViewProjectionMatrix*vec4(localPos,1);
frag/=frag.w;
return (frag.z+1)/2;
}
void main(){
vec2 uv = coord2uv(gl_FragCoord.xy);
vec3 ro = eyePos();
vec3 rd = uv2ray(uv);
vec3 nearest;
float material;
if(marching(ro,rd,nearest,material)){
gl_FragDepth = procDepth(nearest);
gl_FragColor = vec4(vec3(material),1);
}else{
discard;
}
}
添加光照模型与色彩,使用ADS光照模型(ambient,diffuse,specular) 进行渲染。
#define NORMALESP 0.001
vec3 procNormal(vec3 marchpt)
{
float dist = map(marchpt).x;
return normalize(vec3(
dist-map(marchpt-vec3(NORMALESP,0,0)).x,
dist-map(marchpt-vec3(0,NORMALESP,0)).x,
dist-map(marchpt-vec3(0,0,NORMALESP)).x
));
}
vec3 lightModel(vec3 eye,vec3 normal, vec3 light,float material){
float NL = max(dot(normal,light),0);
float RL = max(dot(eye,reflect(light,normal)),0);
return vec3(material*0.2)+vec3(material)*NL+ vec3(material)*pow(RL,5);
}
//
void main(){
vec2 uv = coord2uv(gl_FragCoord.xy);
vec3 ro = eyePos();
vec3 rd = uv2ray(uv);
vec3 nearest;
float material;
if(marching(ro,rd,nearest,material)){
vec3 normal = procNormal(nearest);
gl_FragDepth = procDepth(nearest);
gl_FragColor = vec4(lightModel(rd,normal,lightPos(),material)*normal,1);
}else{
discard;
}
}
基于以上代码着色器,渲染的伪代码如下所示
//伪代码描述
Shader sphereShd;
Mesh cube;
void init(){
//构建一个-1到-1的正方体
cube.buildCube(-1,1);
//加载frag文件生成Shader
sphereShd.loadFrag("sphere_frag.glsl");
sphereShd.link();
}
float offsetx = -0.8;
float offsety = -0.8;
void render(){
glPushMatrix();
glUseProgram(sphereShd.id);
cube.draw();
glUseProgram(0);
gltranslatef(offsetx ,offsety ,0);
cube.draw();
glPopMatrix();
}
调整offset 渲染效果如何所示,可以观察到两物体相交时深度测试符合预期
实际应用
1.渲染多个模型
不同的半径和不同的位置:设置不同的translate rotation scale 多次渲染 伪代码如下
//伪代码描述
Shader sphereShd;
Mesh cube;
void init(){
//构建一个-1到1的正方体
cube.buildCube(-1,1);
//加载frag文件生成Shader
sphereShd.loadFrag("sphere_frag.glsl");
sphereShd.link();
}
float offset = 1.0f;
void render(){
glUseProgram(sphereShd.id);
for(int i=1;i<=5;i++){
glPushMatrix();
glTranslatef(0,(i-1)*5,0);
glScalef(i,i,i);
cube.draw();
glPopMatrix();
}
glUseProgram(0);
}
每次渲染cube之前设置位置和缩放本质上都在改变Shader代码中的gl_ModelViewMatrixInverse,从而产生正确位置大小的球。
使用raymarch shader的多对象渲染流程和普通的片面模型的流程是一致的,不同之处在于raymarch不需要外部顶点数据传入, 与面片模型渲染相比较,优点是更加适用于曲面渲染以及更加灵活、细致的表面着色,缺点是没有类似autodesk的建模工具,要求设计师拥有扎实的数学建模功底。
需要注意的是该体系下不建议使用 XYZ不一致的缩放 比如glScalef(1,1,0.5)。不一致的缩放会产生不合适的光照颜色,原因在于本地坐标体系下的Normal计算,详细请搜索“gl_NormalMatrix”。
2.使用其他距离函数绘制复杂的模型
更改模型的同时需要调整渲染载体(指上文代码中的 Mesh cube)。渲染载体是一个封闭的面片模型对象,要求在几何意义上包含绘制模型,为了避免渲染不完整;要求体积尽可能小,减少需要计算并丢弃的像素个数。通常做法将 cube 设置成渲染模型的 Border Box。 以下代码来自ShaderToy 渲染一个卡通车。
float sdBox(vec3 p, vec3 radius)
{
vec3 dist = abs(p) - radius;
return min(max(dist.x, max(dist.y, dist.z)), 0.0) + length(max(dist, 0.0));
}
float cylCap(vec3 p, float r, float lenRad)
{
float a = length(p.xy) - r;
a = max(a, abs(p.z) - lenRad);
return a;
}
// k should be negative. -4.0 works nicely.
// smooth blending function
float smin(float a, float b, float k)
{
return log2(exp2(k*a)+exp2(k*b))/k;
}
float Repeat(float a, float len)
{
return mod(a, len) - 0.5 * len;
}
vec2 matmin(vec2 v1,vec2 v2){
return v1.x>v2.x?v2:v1;
}
vec2 Car(vec3 baseCenter)
{
// bottom box
float car = sdBox(baseCenter + vec3(0.0,0.01,-0.08), vec3(0.1,0.275,0.0225));
// top box smooth blended
car = smin(car, sdBox(baseCenter + vec3(0.0, 0.08,-0.16), vec3(0.05, 0.1, 0.005)), -16.0);
// mirror the z axis to duplicate the cylinders for wheels
vec3 wMirror = baseCenter + vec3(0.0, 0.0,-0.05);
wMirror.y = abs(wMirror.y)-0.2;
float wheels = cylCap((wMirror).yzx, 0.04, 0.135);
// Set materials
vec2 distAndMat = vec2(wheels, 0.1); // car wheels
// Car material is some big number that's unique to each car
// so I can have each car be a different color
distAndMat = matmin(distAndMat, vec2(car,0.5)); // car
return distAndMat;
}
vec2 map(vec3 pos){
return Car(pos);
}
总结
Depth-RayMarching方案到目前为止比较理想,并且存在较广的应用空间。但细心的读者可能发现了 图片中模型内部,模型与其他模型,模型与环境,这些关系中的锯齿问题比较突出。并且这个问题是由片元着色器所产生的,所以 glEnable(GL_MULTISAMPLE) 并不有效。抗锯齿的实现将会在下一篇博客中阐述。(博主还在研究中... 需要等待)