Chinese Ink-wash Painting II Shader
简介
本文尝试对Chinese Ink-wash Painting II(来源:https://www.shadertoy.com/view/DdSyDW)
的代码部分进行解析,以学习其中实现思路和方法。
代码实现
下面代码首先定义了一些函数来生成噪声、计算光线追踪、获取法线等。然后在mainImage函数中,它计算了每个像素的颜色值,这个颜色值基于光线追踪的结果和噪声纹理。最后,它将计算出的颜色值赋给了fragColor,这就是最终渲染到屏幕上的颜色。
1.常量
头文件中的常量
,用于。
// 定义一些常量
#define MAX_STEPS 200
#define MAX_DIST 100.
#define SURF_DIST .001
#define TAU 6.283185
#define PI 3.141592
#define S smoothstep
#define T iTime
2.哈希值生成
生成2D哈希值:哈希函数可以将输入映射到一个固定大小的数值域,这里的哈希函数将2D坐标映射到一个随机的2D向量
。这个函数在生成噪声函数中被用来创建一个伪随机的、看起来平滑的噪声纹理。
// 生成哈希值
vec2 hash22(vec2 p){
p = vec2( dot(p,vec2(127.1,311.7)),
dot(p,vec2(269.5,183.3)));
return -1.0 + 2.0 * fract(sin(p)*43758.5453123);
}
3.生成噪声和纹理
生成噪声:噪声函数是用来生成一种随机但连续的纹理
,这种纹理常常被用来模拟自然界的各种现象,如云彩、火焰、水波等。
// 生成噪声
float noise(vec2 p)
{
const float K1 = 0.366025404; // (sqrt(3)-1)/2;
const float K2 = 0.211324865; // (3-sqrt(3))/6;
vec2 i = floor(p + (p.x + p.y) * K1);
vec2 a = p - (i - (i.x + i.y) * K2);
vec2 o = (a.x < a.y) ? vec2(0.0, 1.0) : vec2(1.0, 0.0);
vec2 b = a - (o - K2);
vec2 c = a - (1.0 - 2.0 * K2);
vec3 h = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), 0.0);
vec3 n = h * h * h * h * vec3(dot(a, hash22(i)), dot(b, hash22(i + o)), dot(c, hash22(i + 1.0)));
return dot(vec3(70.0, 70.0, 70.0), n);
}
// 生成噪声纹理
float noise_itself(vec2 p)
{
float f = 0.;
p *= 8.;
f += 1.0000 * noise(p); p = 6.0 * p;
f += 0.1000 * noise(p); p = 4.0 * p;
f += 0.0100 * noise(p); p = 3.0 * p;
return f;
}
4.旋转矩阵
旋转矩阵:用来进行坐标旋转的,它可以将一个坐标旋转到一个新的位置。在场景中,旋转矩阵被用来旋转相机的方向
,使得用户可以从不同的角度观察地形。
// 旋转矩阵
mat2 Rot(float a) {
float s=sin(a), c=cos(a);
return mat2(c, -s, s, c);
}
5.获取距离
下面展示一些 内联代码片
。
// 获取距离
float GetDist(vec3 p) {
float d = p.y+4.;
p.z -= iTime;
float h = max(0., noise_itself(p.xz*0.01+13.7));
h = pow(h, 1.6)*8.;
d -= h;
return d*0.3;
}
6.光线追踪
光线追踪是一种用于生成图像的技术,通过追踪光线从视点出发并穿过像素投射到场景中的路径,来计算每个像素的颜色
。
// 光线追踪
float RayMarch(vec3 ro, vec3 rd) {
float dO=0.;
for(int i=0; i<MAX_STEPS; i++) {
vec3 p = ro + rd*dO;
float dS = GetDist(p);
dO += dS;
if(dO>MAX_DIST || abs(dS)<SURF_DIST) break;
}
return dO;
}
7.获取法线
法线向量在计算机图形学中有很多重要的应用,主要包括以下几个方面:
-
光照计算:在计算物体表面的光照效果时,法线向量是非常关键的一个因素。根据光源方向和法线向量的夹角,可以决定物体表面的亮度。
-
碰撞检测:在物理模拟和游戏开发中,法线向量常常用于碰撞检测和碰撞响应。当两个物体发生碰撞时,可以通过法线向量来计算反弹的方向。
-
纹理映射:在进行纹理映射时,法线向量可以用于计算纹理的方向,使得纹理能够正确地贴在物体表面上。
-
几何处理:在进行几何处理,如模型剪裁、隐藏面消除等操作时,法线向量也会被用到。
vec3 GetNormal(vec3 p) {
vec2 e = vec2(.001, 0);
vec3 n = GetDist(p) -
vec3(GetDist(p-e.xyy), GetDist(p-e.yxy),GetDist(p-e.yyx));
return normalize(n);
}
8.获取光线方向
在光线追踪或者光线投射的算法中,根据光线的起点、目标点和焦距,以及2D的纹理坐标,来计算光线的方向
。
// 获取光线方向
vec3 GetRayDir(vec2 uv, vec3 p, vec3 l, float z) {
vec3
f = normalize(l-p),
r = normalize(cross(vec3(0,1,0), f)),
u = cross(f,r),
c = f*z,
i = c + uv.x*r + uv.y*u;
return normalize(i);
}
9.主函数
主函数:GLSL(OpenGL Shading Language)中的主函数(通常命名为main)在片元着色器中负责计算每个像素的颜色。是程序的入口点,它是程序执行的开始。在这个上下文中,mainImage
函数是一个特殊的主函数,它是在渲染管线中被调用的,用于计算每个像素的颜色值。
当你的图形程序运行时,GPU会为屏幕上的每个像素调用一次片元着色器。在每次调用中,主函数都会计算出当前像素的颜色,并将其存储在gl_FragColor变量中。这样,当所有的像素都经过片元着色器处理后,你就得到了最终的图像。
需要注意的是,虽然主函数会被调用多次(每个像素一次),但每次调用都是独立的,也就是说,每次调用只会影响当前的像素,不会影响其他像素的颜色
本Shader首先根据输入的屏幕坐标和分辨率,计算出每个像素对应的UV坐标。然后,根据鼠标的位置,计算出相机的位置和方向。接着,根据UV坐标和相机的位置和方向,计算出光线的方向。然后,使用光线追踪算法,计算出光线与地形的交点,以及交点处的颜色。最后,根据太阳的亮度,调整颜色,并将计算出的颜色赋给输出的颜色,计算出每个像素的颜色后,从而生成一个3D地形的渲染图像。
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// 将屏幕坐标转换为[-1, 1]范围内的UV坐标
vec2 uv = (fragCoord-.5*iResolution.xy)/iResolution.y;
// 设置默认的鼠标位置
vec2 m = vec2(-.05, -0.5);
// 如果鼠标在屏幕上移动,则更新鼠标位置
if(iMouse.z > 0.) m = iMouse.xy/iResolution.xy;
// 设置相机的初始位置
vec3 ro = vec3(0, 3, -3);
// 根据鼠标的位置旋转相机的方向
ro.yz *= Rot(-m.y*PI+1.);
ro.y = max(ro.y, -1.);
ro.xz *= Rot(-m.x*TAU);
// 计算光线的方向
vec3 rd = GetRayDir(uv, ro, vec3(0,0.,0), 1.);
// 初始化颜色为黑色
vec3 col = vec3(0);
// 进行光线追踪,获取光线行进的距离
float d = RayMarch(ro, rd);
// 计算太阳的亮度
float sun = S(0.998,1.,dot(normalize(rd), normalize(vec3(0.,0.5,-1.))));
// 如果光线没有超出最大距离,说明光线击中了物体
if(d<MAX_DIST) {
// 计算光线击中的位置
vec3 p = ro + rd * d;
// 计算该位置的法线
vec3 n = GetNormal(p);
// 设置光源的方向
vec3 lightDir = normalize(vec3(0.0, .5,-.5));
// 计算菲涅尔项,用于模拟光线在表面的反射和折射,描述光线在接触物体表面时,反射和折射
float fre = max(0.,dot(n, normalize(ro-p)));
fre = pow(fre,.125);
fre = S(0.8,0.85,fre);
fre = mix(fre, 1., S(-0.0, -4., p.y));
// 根据高度计算颜色
float heigh = S(-4., 0., p.y);
col = mix(vec3(0.6588,0.5176,0.2824), vec3(0.3059, 0.4627, 0.633), heigh)*fre;
// 如果高度在一定范围内,修改颜色
heigh = S(-4.1, -3.9, p.y);
col = mix(vec3(0.6,0.55,0.2)*S(0.,15., d), col, heigh);
// 将太阳亮度设置为0
sun = .0;
}
// 如果光线没有击中物体,设置背景颜色
col = mix(col, vec3(0.6588,0.5176,0.2824)*1.5, S(0.,100., d));
// 根据太阳的亮度调整颜色
col = mix(col, vec3(1.,0.4,0.), sun);
// 设置最终的颜色
fragColor = vec4(col, 1.);
}
这个函数的主要目的是计算每个像素的颜色。
流程:首先计算出光线的方向,然后进行光线追踪,如果光线击中了物体,就根据物体的属性(如法线、高度等)来计算颜色,否则就设置为背景色。最后,根据太阳的亮度来调整颜色,并将计算出的颜色赋给fragColor。
10.总结
这几部分代码的主要目的都是为了生成一个看起来真实的3D地形。通过使用噪声函数和噪声纹理,可以生成一个既随机又自然的地形。通过使用旋转矩阵,可以让用户从不同的角度观察这个地形。
其他
噪声在计算机图形学中是一种非常重要的工具,因为它可以生成一种随机但连续的纹理,这种纹理在视觉上看起来非常类似于自然界的许多现象。如山脉、河流、云彩、火焰、水波等,都具有一种随机但又有规律的特性。这种特性很难通过简单的数学函数来模拟,但是噪声函数可以很好地模拟出这种特性。
噪声函数生成的纹理既有随机性,又有连续性,这使得它可以用来模拟自然界的各种现象。例如,我们可以通过噪声函数生成地形的高度值,使得地形看起来既随机又自然。我们也可以通过噪声函数生成云彩的形状,使得云彩看起来既随机又连续。
因此,噪声函数可以用来模拟自然界的各种现象,从而生成更加真实和生动的图像。