ShaderToy入门初体验
前言
[shadertoy](https://www.shadertoy.com/)
那些大神写的demo简直太震撼了。
第一个程序
点击上图中的“新建”,弹出下图界面
代码详解
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = fragCoord/iResolution.xy;
// Time varying pixel color
vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
// Output to screen
fragColor = vec4(col,1.0);
}
这是ShaderToy的主函数mainImage,
第一个参数出:out vec4 fragColor,输出的是像素的颜色向量。
第二个参数入in: vec2 fragCoord,输入的像素坐标向量,主要作用就是根据屏幕上的像素坐标,算出像素的颜色向量,简单来说完成像素坐标到颜色的变换或者是映射。屏幕分辨率是800乘600的话,就计算800乘600区域内的所有像素。ShaderToy当前窗口的每个像素坐标都要经过这个主函数的处理以决定其颜色,所以看似这个主程序是一段代码,其实逻辑上被嵌在了一个像素坐标的大循环里面。
注意!!!
shadertoy有很多内置变量,这些我们用户不能重定义
/**
*常量定义
*/
//uniform vec3 iResolution; // 窗口分辨率,单位像素
//uniform float iTime; // 程序运行的时间,单位秒
//uniform float iTimeDelta; // 渲染时间,单位秒
//uniform float iFrame; // 帧率
//uniform vec4 iMouse; // 鼠标位置
//uniform vec4 iDate; // 日期(年,月,日,时)
//uniform sampler2D iChannel0 //获取纹理像素颜色
//uniform float iChannelTime[4];
//uniform samplerXX iChanneli;
//uniform SampleRate; //指定的采样率进行采样,根据应用程序,该采样率通常为44100或48000。用于声音着色器
vec2 mainSound( float time )
通过mainSound函数的返回值,将期望的波幅输出为立体声(左和右声道)声音的一对值。
由mainSound入口点生成的着色器将被自动标记为声音着色器,并且可以通过声音输出限定词的过滤器系统进行搜索。
vec2 uv = fragCoord/iResolution.xy;```
uv这里做了归一化处理, uv.x, uv.y的取值都在0~1;
像素颜色按时间步进变化
vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));
小知识:shadertoy里面计算距离用到SDF
有符号距离场(Signed Distance Field,SDF)
SDF的用途很多,可以用来做large scale AO, 软阴影等。UE4就用SDF来做AO和软阴影,SDF Ray-traced shaow 比PCSS软,而且和CSM相比,因为没有那么大的几何填充负担,所以反而要便宜很多,和静态的Shadow map相比,又可以支持物体级别的移动(虽然不支持顶点动画),所以是一个相当不错的阴影解决方案。堡垒之夜就用了这项技术,堡垒之夜的近处是级联阴影,远处是SDF Ray-traced shadow.
符号距离函数Signed Distance Function是某度量空间X中的一个集合ΩΩ的函数,决定X中任一点到 ΩΩ边界∂Ω∂Ω的距离,并且由x是在ΩΩ 内还是ΩΩ外确定其SDF的正负号:当x在ΩΩ内时,SDF为正;当x在ΩΩ外时,SDF为负。假设d是空间X的一种度量,那么SDF用数学公式表达:
f(x)={d(x,∂Ω)−d(x,∂Ω)if x∈Ωif x∈Ωc
f(x)={d(x,∂Ω)if x∈Ω−d(x,∂Ω)if x∈Ωc
这种函数可以用来表示曲线:f(x)=0f(x)=0。
光线步进——RayMarching
RayMarching 是一种用于实时场景的快速渲染方法,我的理解是,模拟摄像机位置,根据视椎体的扩张角度,以摄像机位置为原点,进步式发射射线,当射线碰撞到物体之后,返回其深度信息,如果到视椎体的最大距离之前都没有返回,那么可以以此判断该像素点没有对于物体,最后根据返回的信息计算光照。
可以看出,RayMarching是有误差的,如果提高精度,减少步长,循环次数又太多,导致效率很低。
感觉RayMarching目前用来渲染云,雾这些类似的体积渲染比较多。
创建一个camera
我们需要去定义camera的origin,target,和up 就是定义摄像机的起源,目标位置,还有就是定义向上的位置。
vec3 cameraOrigin = vec3(2.0, 3.0, 2.0);
vec3 cameraTarget = vec3(0.0, 0.0, 0.0);
vec3 upDirection = vec3(0.0, 1.0, 0.0);
然后就可以得出摄像机的方向是:
两个向量相减的物理意义:是得到方向
vec3 cameraDir = normalize(cameraTarget - cameraOrigin);
由此可以计算出摄像机的右方向和顶上的方向。
叉乘 cross(a,b)的物理意义:得到a和b的法线
vec3 cameraRight = normalize(cross(upDirection, cameraOrigin));
vec3 cameraUp = cross(cameraDir, cameraRight);
下面对屏幕坐标进行转化,把屏幕坐标放缩到-1到1之间。
vec2 screenPos = -1.0 + 2.0 * gl_FragCoord.xy / iResolution.xy; // screenPos can range from -1 to 1
screenPos.x *= iResolution.x / iResolution.y; // Correct aspect ratio
在知道了摄像机的方向之后,我们来计算出ray的方向。
vec3 rayDir = normalize(cameraRight * screenPos.x + cameraUp * screenPos.y + cameraDir);
Raymarching loop
在 marching 里面 先来设置步进的光线总长度
const int MAX_ITER = 100;
物体的离摄像机的最大范围
const float MAX_DIST = 20.0;
设置物体离光线的阈值距离
const float EPSILON = 0.001;
下面是loop的代码:在个里面点会被转化为和交集的东西。
// The raymarching loop
float totalDist = 0.0;
vec3 pos = cameraOrigin;
float dist = EPSILON;
// trying to find a point of intersection
for (int i = 0; i < MAX_ITER; i++)
{
// Either we've hit the object or hit nothing at all, either way we should break out of the loop
if (dist < EPSILON || totalDist > MAX_DIST)
break; // If you use windows and the shader isn't working properly, change this to continue;
dist = distfunc(pos); // Evalulate the distance at the current point
totalDist += dist;
pos += dist * rayDir; // Advance the point forwards in the ray direction by the distance
}
定义显示的模型
float sphere(vec3 pos, float radius)
{
return length(pos) - radius;
}
float box(vec3 pos, vec3 size)
{
return length(max(abs(pos) - size, 0.0));
}
定义一下Lighting
光也要在EPSILON的距离里面
if (dist < EPSILON)
{
// Lighting code
}
else
{
gl_FragColor = vec4(0.0);
}
光函数里面需要取得表面着色器的normal向量,可以用点来预计算出点的位置。
vec2 eps = vec2(0.0, EPSILON);
vec3 normal = normalize(vec3(
distfunc(pos + eps.yxx) - distfunc(pos - eps.yxx),
distfunc(pos + eps.xyx) - distfunc(pos - eps.xyx),
distfunc(pos + eps.xxy) - distfunc(pos - eps.xxy)));
由光照公式可以得出
fragColor = vec4(ambientColor +
lambertian * diffuseColor +
specular * specColor, 1.0);
原理
从摄像机位置向屏幕每一个像素点发射一条光线,光线按照一定步长前进,并检测当前光线是否位于物体表面,据此调整光线前进幅度,直到抵达物体表面,再按照一般光线追踪的方法计算颜色值。
步骤
1.定义摄像机的位置,光线的方向;
2.定义场景:场景包含一个球体和平面;
3.定义灯光的方向;
4.计算法线的方向;
// 三维坐标:左手坐标系,X轴指向屏幕右侧,Y轴指向屏幕上面,Z轴垂直于屏幕向里
现在开始,我们定义摄像机的位置为 x = 0, y = 1, z = 0;方向是屏幕每个像素点的方向normalize(vec3(uv.x,uv.y,1.));
// Camera:ro 位置, rd 方向
vec3 ro = vec3(0,1,0);
vec3 rd = normalize(vec3(uv.x,uv.y,1.));
我们的场景包含一个球体和一个平面:平面是xz平面,高度 y=0
vec4 s = vec4(0,1,6,1);// 球的位置(s.xyz)和半径(s.w)
我们现在计算光线步进的距离,每一次向前步进一段距离,这个距离是当前点和场景中所有物体的相交的最小距离;如果前进的总距离大于我们要的最大距离或者当前前进的距离小于一个常数,我们认为这个步进的长度没必要继续进行下去,我们就终止循环。
#define Max_Steps 100 // 最大步数
#define Max_Dist 100. // 最大距离
#define Surf_Dist 0.01 //
// 获取当前点和场景中是所有物体相交的最小距离
float GetDist(vec3 p)
{
vec4 s = vec4(0,1,6,1);// 球的位置(s.xyz)和半径(s.w)
float sphereDist = length(p-s.xyz)-s.w;// P点到球面的距离
float planeDist = p.y;// P点到平面的距离,平面是xz平面,高度y = 0;
float d = min(sphereDist,planeDist);
return d;
}
float RayMarch(vec3 ro, vec3 rd)
{
float d0 = 0.;
for(int i = 0; i < Max_Steps; i++)
{
vec3 p = ro + rd*d0;
float ds = GetDist(p);
d0+=ds;
if(d0>Max_Dist || ds < Surf_Dist)
break;
}
return d0;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord-0.5*iResolution.xy)/iResolution.y;
vec3 col = vec3(0);
// Camera:ro 位置, rd 方向
vec3 ro = vec3(0,1,0);
vec3 rd = normalize(vec3(uv.x,uv.y,1.));
float d = RayMarch(ro,rd);
d/=6.;
col = vec3(d);
// Output to screen
fragColor = vec4(col,1.0);
}
如图所示,使用返回的距离来描述场景。如果RGBA颜色值是[0,1]区间,距离越远越接近1,则越白;越近越接近0,则越黑。摄像机与球表面最近的点的距离是d=5,现在我们除以一个数,将球显示得更黑,这里我们除以6,即摄像机与球心的距离。
这个时候我们需要加入灯光,计算阴影。灯光照射的方向与顶点的法线的点积是在该点的光照强度的影响值。我们假设灯光的方向是 vec3(0,5,6),现在我们计算顶点的法线;
vec3 lightPos = vec3(0,5,6);
顶点法线的计算
// 顶点的法线
vec3 GetNormal(vec3 p)
{
float d = GetDist(p);
vec2 e = vec2(0.01,0);
vec3 n = d-vec3(
GetDist(p-e.xyy),
GetDist(p-e.yxy),
GetDist(p-e.yyx));
return normalize(n);
}
有了法线,我们就可以计算灯光了
float GetLight(vec3 p)
{
vec3 lightPos = vec3(0,5,6);
lightPos.xz += vec2(sin(iTime),cos(iTime))*2.0;
vec3 l = normalize(lightPos-p);// 点光源
vec3 n = GetNormal(p);
float dif = clamp(dot(n,l),0.,1.);//漫反射颜色
return dif;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord-0.5*iResolution.xy)/iResolution.y;
vec3 col = vec3(0);
// Camera:ro 位置, rd 方向
vec3 ro = vec3(0,1,0);
vec3 rd = normalize(vec3(uv.x,uv.y,1.));
float d = RayMarch(ro,rd);
vec3 p = ro + rd*d;
float dif = GetLight(p);
col = vec3(dif);
// Output to screen
fragColor = vec4(col,1.0);
}
但是目前灯光只是在球面上,应该在平面上也投上阴影。
原理:步进下的每一个点出发,沿着灯光方向步进,如果距离小于灯光到点的距离,那么证明碰到了东西。这点点就在阴影下。
阶段效果
改良光照计算:
// 计算灯光,点光源
float GetLight(vec3 p)
{
vec3 lightPos = vec3(0,5,6);
lightPos.xz += vec2(sin(iTime),cos(iTime))*2.0;
vec3 l = normalize(lightPos-p);
vec3 n = GetNormal(p);
float dif = clamp(dot(n,l),0.,1.);
float d = RayMarch(p + n*Surf_Dist*2.0,l);
if(d<length(lightPos-p))dif*=0.1;
return dif;
}
完整代码
/******** 原理
/********从摄像机位置向屏幕每一个像素点发射一条光线,光线按照一定步长前进,
/********并检测当前光线是否位于物体表面,据此调整光线前进幅度,直到抵达物体表面,
/********再按照一般光线追踪的方法计算颜色值。*/
// 屏幕左下角UV(0,0) 右上角UV(1,1)
// 三维坐标:左手坐标系,X轴指向屏幕右侧,Y轴指向屏幕上面,Z轴垂直于屏幕向里
/**
*常量定义
*/
/**
*常量定义
*/
//uniform vec3 iResolution; // 窗口分辨率,单位像素
//uniform float iTime; // 程序运行的时间,单位秒
//uniform float iTimeDelta; // 渲染时间,单位秒
//uniform float iFrame; // 帧率
//uniform vec4 iMouse; // 鼠标位置
//uniform vec4 iDate; // 日期(年,月,日,时)
// 最大步数
// 最大距离
// 最小步进的距离
#define Max_Steps 100
#define Max_Dist 100.
#define Surf_Dist 0.01
//获取距离
float GetDist(vec3 p)
{
vec4 s = vec4(0,1,6,1);// 球的位置(s.xyz)和半径(s.w)
float sphereDist = length(p-s.xyz)-s.w;// P点到球面的距离
float planeDist = p.y;// P点到平面的距离
float d = min(sphereDist,planeDist); //球体中心点到平面的最小距离
return d;
}
//光线步进
float RayMarch(vec3 ro, vec3 rd)
{
float d0 = 0.;
for(int i = 0; i < Max_Steps; i++)
{
vec3 p = ro + rd*d0;
float ds = GetDist(p);
d0+=ds;
if(d0>Max_Dist || ds < Surf_Dist)
break;
}
return d0;
}
// 顶点的法线
vec3 GetNormal(vec3 p)
{
float d = GetDist(p);
vec2 e = vec2(0.01,0);
vec3 n = d-vec3(
GetDist(p-e.xyy),
GetDist(p-e.yxy),
GetDist(p-e.yyx));
return normalize(n);
}
// 计算灯光,点光源
float GetLight(vec3 p)
{
vec3 lightPos = vec3(0,5,6);
lightPos.xz += vec2(sin(iTime),cos(iTime))*2.0;
vec3 l = normalize(lightPos-p);
vec3 n = GetNormal(p);
float dif = clamp(dot(n,l),0.,1.);
float d = RayMarch(p + n*Surf_Dist*2.0,l);
if(d<length(lightPos-p))dif*=0.1;
return dif;
}
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord-0.5*iResolution.xy)/iResolution.y;
vec3 col = vec3(0);
// Camera:ro 位置, rd 方向
vec3 ro = vec3(0,1,0);
vec3 rd = normalize(vec3(uv.x,uv.y,1.));
float d = RayMarch(ro,rd);
vec3 p = ro + rd*d;
float dif = GetLight(p);
//d/=6.0;
col = vec3(dif);
// Output to screen
fragColor = vec4(col,1.0);
}
运行效果
参考引用机器猫
这里感谢
机器猫