水墨Shader解析

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.获取法线

法线向量在计算机图形学中有很多重要的应用,主要包括以下几个方面:

  1. 光照计算:在计算物体表面的光照效果时,法线向量是非常关键的一个因素。根据光源方向和法线向量的夹角,可以决定物体表面的亮度。

    扫描二维码关注公众号,回复: 16753929 查看本文章
  2. 碰撞检测:在物理模拟和游戏开发中,法线向量常常用于碰撞检测和碰撞响应。当两个物体发生碰撞时,可以通过法线向量来计算反弹的方向。

  3. 纹理映射:在进行纹理映射时,法线向量可以用于计算纹理的方向,使得纹理能够正确地贴在物体表面上。

  4. 几何处理:在进行几何处理,如模型剪裁、隐藏面消除等操作时,法线向量也会被用到。

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地形。通过使用噪声函数和噪声纹理,可以生成一个既随机又自然的地形。通过使用旋转矩阵,可以让用户从不同的角度观察这个地形。

其他

噪声在计算机图形学中是一种非常重要的工具,因为它可以生成一种随机但连续的纹理,这种纹理在视觉上看起来非常类似于自然界的许多现象。如山脉、河流、云彩、火焰、水波等,都具有一种随机但又有规律的特性。这种特性很难通过简单的数学函数来模拟,但是噪声函数可以很好地模拟出这种特性。

噪声函数生成的纹理既有随机性,又有连续性,这使得它可以用来模拟自然界的各种现象。例如,我们可以通过噪声函数生成地形的高度值,使得地形看起来既随机又自然。我们也可以通过噪声函数生成云彩的形状,使得云彩看起来既随机又连续。

因此,噪声函数可以用来模拟自然界的各种现象,从而生成更加真实和生动的图像。

猜你喜欢

转载自blog.csdn.net/m0_51947486/article/details/132058882