Last active
January 17, 2023 14:10
-
-
Save HK-SHAO/e142e3889270d353e3acae5bf3e1a82a to your computer and use it in GitHub Desktop.
RayTracing PBR with Free Camera in ShaderToy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright © 2019-2023 HK-SHAO | |
// MIT Licensed: https://shao.fun/blog/w/taichi-ray-tracing.html | |
#define load(P) texelFetch(iChannel1, ivec2(P), 0) | |
// 光线 | |
struct ray { | |
vec3 origin; // 光的起点 | |
vec3 direction; // 光的方向 | |
vec3 color; // 光的颜色 | |
}; | |
// 物体材质 | |
struct material { | |
vec3 albedo; // 反照率 | |
vec3 emission; // 自发光 | |
float roughness; // 粗糙度 | |
float metallic; // 金属度 | |
float transmission; // 透明度 | |
float ior; // 折射率 | |
}; | |
// 物体变换 | |
struct transform { | |
vec3 position; // 位置 | |
vec3 rotation; // 旋转 | |
vec3 scale; // 缩放 | |
}; | |
// SDF 物体 | |
struct object { | |
int shape; // 形状 | |
float dis; // 距离物体表面 | |
transform trs; // 变换 | |
material mtl; // 材质 | |
}; | |
// 光子击中的记录 | |
struct record { | |
object obj; // 物体 | |
vec3 pos; // 击中的位置 | |
vec3 normal; // 世界空间法线 | |
bool hit; // 是否击中 | |
}; | |
// 摄像机 | |
struct camera { | |
vec3 lookfrom; // 视点位置 | |
vec3 lookat; // 目标位置 | |
vec3 vup; // 向上的方向 | |
float vfov; // 视野角度 | |
float aspect; // 传感器长宽比 | |
float aperture; // 光圈大小 | |
float focus; // 对焦距离 | |
}; | |
// 地图列表 | |
const object[] map = object[] ( | |
object(SHAPE_SPHERE, ZERO, | |
transform( vec3(0, -100.501, 0), | |
vec3(0, 0, 0), | |
vec3(100, 100, 100) | |
), | |
material( vec3(1.0, 1.0, 1.0)*0.6, // 基础色 | |
vec3(1), // 自发光 | |
0.9, // 粗糙度 | |
0.1, // 金属度 | |
0.0, // 透明度 | |
1.635 // 折射率 (沥青) | |
) | |
), | |
object(SHAPE_SPHERE, ZERO, | |
transform( vec3(0, 0, 0), | |
vec3(0, 0, 0), | |
vec3(0.5, 0.5, 0.5) | |
), | |
material( vec3(1.0, 1.0, 1.0), // 基础色 | |
vec3(0.1, 1.0, 0.1)*10.0, // 自发光 | |
0.0, // 粗糙度 | |
1.0, // 金属度 | |
0.0, // 透明度 | |
1.000 // 折射率 (真空) | |
) | |
), | |
object(SHAPE_CYLINDER, ZERO, | |
transform( vec3(-1.0, -0.2, 0), | |
vec3(0, 0, 0), | |
vec3(0.3, 0.3, 0.3) | |
), | |
material( vec3(1.0, 0.2, 0.2), // 基础色 | |
vec3(1), // 自发光 | |
0.1, // 粗糙度 | |
0.0, // 金属度 | |
0.0, // 透明度 | |
1.460 // 折射率 (塑料) | |
) | |
), | |
object(SHAPE_SPHERE, ZERO, | |
transform( vec3(1.0, -0.2, 0), | |
vec3(0, 0, 0), | |
vec3(0.3, 0.3, 0.3) | |
), | |
material( vec3(0.2, 0.2, 1.0), // 基础色 | |
vec3(1), // 自发光 | |
0.2, // 粗糙度 | |
1.0, // 金属度 | |
0.0, // 透明度 | |
1.100 // 折射率 (铜) | |
) | |
), | |
object(SHAPE_SPHERE, ZERO, | |
transform( vec3(0.0, -0.2, 2), | |
vec3(0, 0, 0), | |
vec3(0.3, 0.3, 0.3) | |
), | |
material( vec3(1.0, 1.0, 1.0)*0.9, // 基础色 | |
vec3(1), // 自发光 | |
0.0, // 粗糙度 | |
0.0, // 金属度 | |
1.0, // 透明度 | |
1.500 // 折射率 (玻璃) | |
) | |
), | |
object(SHAPE_BOX, ZERO, | |
transform( vec3(0, 0, 5), | |
vec3(0, 0, 0), | |
vec3(2, 1, 0.2) | |
), | |
material( vec3(1.0, 1.0, 0.2)*0.9, // 基础色 | |
vec3(1), // 自发光 | |
0.0, // 粗糙度 | |
1.0, // 金属度 | |
0.0, // 透明度 | |
0.470 // 折射率 (金) | |
) | |
), | |
object(SHAPE_BOX, ZERO, | |
transform( vec3(0, 0, -1), | |
vec3(0, 0, 0), | |
vec3(2, 1, 0.2) | |
), | |
material(vec3(1.0, 1.0, 1.0)*0.9, // 基础色 | |
vec3(1), // 自发光 | |
0.0, // 粗糙度 | |
1.0, // 金属度 | |
0.0, // 透明度 | |
2.950 // 折射率 (铁) | |
) | |
) | |
); | |
// 光子在射线所在的位置 | |
vec3 at(ray r, float t) { | |
return r.origin + t * r.direction; | |
} | |
// 单位圆内随机取一点 | |
vec2 random_in_unit_disk() { | |
vec2 r = rand21() * vec2(1.0, TAU); | |
return sqrt(r.x) * vec2(sin(r.y), cos(r.y)); | |
} | |
// 从摄像机获取光线 | |
ray get_ray(camera c, vec2 uv, vec3 color) { | |
// 根据 VFOV 和显示画布长宽比计算传感器长宽 | |
float theta = radians(c.vfov); | |
float half_height = tan(theta * 0.5); | |
float half_width = c.aspect * half_height; | |
// 以目标位置到摄像机位置为 Z 轴正方向 | |
vec3 z = normalize(c.lookfrom - c.lookat); | |
// 计算出摄像机传感器的 XY 轴正方向 | |
vec3 x = normalize(cross(c.vup, z)); | |
vec3 y = cross(z, x); | |
// 模拟光进入镜头光圈 | |
float lens_radius = c.aperture * 0.5; | |
vec2 rud = lens_radius * random_in_unit_disk(); | |
vec3 offset = x * rud.x + y * rud.y; | |
// 计算半高、半宽、左下角位置等 | |
vec3 hwfx = half_width * c.focus * x; | |
vec3 hhfy = half_height * c.focus * y; | |
vec3 lower_left_corner = c.lookfrom - hwfx - hhfy - c.focus * z; | |
vec3 horizontal = 2.0 * hwfx; | |
vec3 vertical = 2.0 * hhfy; | |
// 计算光线起点和方向 | |
vec3 ro = c.lookfrom + offset; | |
vec3 po = lower_left_corner + uv.x * horizontal + uv.y * vertical; | |
vec3 rd = normalize(po - ro); | |
return ray(ro, rd, color); | |
} | |
// 从欧拉角计算旋转矩阵 | |
mat3 angle(vec3 a) { | |
vec3 s = sin(a), c = cos(a); | |
return mat3(vec3( c.z, s.z, 0), | |
vec3(-s.z, c.z, 0), | |
vec3( 0, 0, 1)) * | |
mat3(vec3( c.y, 0, -s.y), | |
vec3( 0, 1, 0), | |
vec3( s.y, 0, c.y)) * | |
mat3(vec3( 1, 0, 0), | |
vec3( 0, c.x, s.x), | |
vec3( 0, -s.x, c.x)); | |
} | |
// 计算有向距离 (物体内部距离为负) | |
float signed_distance(object obj, vec3 pos) { | |
vec3 position = obj.trs.position; | |
vec3 rotation = obj.trs.rotation; | |
vec3 scale = obj.trs.scale; | |
vec3 p = pos - position; | |
p *= angle(radians(rotation)); | |
switch(obj.shape) { | |
case SHAPE_SPHERE: | |
return sd_sphere(p, scale.x); | |
case SHAPE_BOX: | |
return sd_box(p, scale); | |
case SHAPE_CYLINDER: | |
return sd_cylinder(p, scale.xy); | |
default: | |
return sd_sphere(p, scale.x); | |
} | |
} | |
// 找到最近的物体并计算距离 | |
object nearest_object(vec3 p) { | |
object o; o.dis = TMAX; | |
for (int i = 0; i < map.length(); i++) { | |
object oi = map[i]; | |
oi.dis = abs(signed_distance(oi, p)); | |
if (oi.dis < o.dis) o = oi; | |
} | |
return o; | |
} | |
// 计算物体法线 from https://iquilezles.org/articles/normalsSDF/ | |
vec3 calc_normal(object obj, vec3 p) { | |
vec2 e = vec2(1, -1) * 0.5773 * 0.0005; | |
return normalize( e.xyy*signed_distance(obj, p + e.xyy) + | |
e.yyx*signed_distance(obj, p + e.yyx) + | |
e.yxy*signed_distance(obj, p + e.yxy) + | |
e.xxx*signed_distance(obj, p + e.xxx) ); | |
} | |
// 光线步进检测第一个交点 | |
record raycast(ray r) { | |
record rec; float t = TMIN; | |
for(uint i = 0U; i < MAX_RAYMARCH && t < TMAX && !rec.hit; i++) { | |
rec.pos = at(r, t); | |
rec.obj = nearest_object(rec.pos); | |
rec.hit = rec.obj.dis < PRECISION; | |
t += rec.obj.dis; | |
} | |
return rec; | |
} | |
// 采样立方体贴图 | |
vec4 mix_cube_lod(samplerCube sharp, samplerCube blur, vec3 dir, float lod) { | |
return mix(textureLod(sharp, dir, lod), textureLod(blur, dir, lod), lod); | |
} | |
// 采样天空 | |
vec3 sky(ray r, float brightness, float lod) { | |
vec4 ibl = mix_cube_lod(iChannel2, iChannel3, r.direction, lod); // 天空盒 IBL 照明 | |
return pow(ibl.rgb * ibl.a * brightness, vec3(gamma)); // gamma 矫正 | |
} | |
// 菲涅尔反射近似值 | |
float fresnel_schlick(float cosine, float F0) { | |
return mix(pow(abs(1.0 - cosine), 5.0), 1.0, F0); | |
} | |
// 用粗糙度计算菲涅尔 | |
float fresnel_schlick(float cosine, float F0, float roughness) { | |
return mix(fresnel_schlick(cosine, F0), F0, roughness); | |
} | |
// 在切线空间半球采样 | |
vec3 hemispheric_sampling(vec3 nor) { | |
vec2 r = rand21() * vec2(2.0, TAU) - vec2(1, 0); | |
vec2 xy = sqrt(1.0 - r.x*r.x) * vec2(sin(r.y), cos(r.y)); | |
float z = r.x; | |
return normalize(nor + vec3(xy, z)); | |
} | |
// 用粗糙度改变采样方向 | |
vec3 roughness_sampling(vec3 hemispheric_sample, vec3 nor, float roughness) { | |
float alpha = roughness * roughness; | |
return normalize(mix(nor, hemispheric_sample, alpha)); | |
} | |
// 应用 PBR 材质 | |
ray BSDF(ray r, record rec) { | |
// 材质参数 | |
vec3 albedo = rec.obj.mtl.albedo; | |
float roughness = rec.obj.mtl.roughness; | |
float metallic = rec.obj.mtl.metallic; | |
float transmission = rec.obj.mtl.transmission; | |
float ior = rec.obj.mtl.ior; | |
vec3 normal = rec.normal; | |
bool outer = dot(normal, r.direction) < 0.0; // 光正在从外面穿入物体表面 | |
vec3 I = r.direction; // 入射光方向 | |
vec3 N = normal *= outer ? 1.0 : -1.0; // 如果处于 SDF 物体内部就反过来 | |
vec3 C = r.color; // 光的颜色 | |
vec3 L; // 出射光方向 | |
vec3 hemispheric_sample = hemispheric_sampling(normal); // 切线空间的半球采样方向 | |
N = roughness_sampling(hemispheric_sample, normal, roughness); | |
float NoI = dot(N, I), NoV = -NoI; | |
float eta = outer ? ENV_IOR / ior : ior / ENV_IOR; // 计算折射率之比 | |
float k = 1.0 - eta * eta * (1.0 - NoV * NoV); // 小于 0 为全反射 | |
float F0 = (eta - 1.0) / (eta + 1.0); F0 *= 2.0*F0; // 让透明材质的反射更明显一些 | |
float F = fresnel_schlick(NoV, F0, roughness); // 菲涅尔 | |
vec2 rand2 = rand21(); // 取两个随机数 | |
if (rand2.x < F + metallic || k < 0.0) { | |
L = I - 2.0 * NoI * N; // 包含镜面反射、菲涅尔反射、全反射 | |
// 下面可以提高帧数,但是会导致透明材质发黑,需要优化 | |
C *= float(dot(L, normal) > 0.0); // 如果光穿入或穿出表面就直接吸收掉 | |
} else if (rand2.y < transmission) { | |
L = eta * I - (sqrt(k) + eta * NoI) * N; // 斯涅尔折射 | |
} else { | |
L = hemispheric_sample; // 漫反射 | |
} | |
C *= albedo; | |
// 更新光的方向和颜色 | |
r.color = C; | |
r.direction = L; | |
return r; | |
} | |
// RGB 亮度 | |
float brightness(vec3 rgb) { | |
return dot(rgb, vec3(0.299, 0.587, 0.114)); | |
} | |
// 光线追踪 | |
ray raytrace(ray r) { | |
for (uint i = 0U; i < MAX_RAYTRACE; i++) { | |
// 俄罗斯轮盘赌概率,防止光线过分的反复反射 | |
float inv_pdf = exp(float(i) / light_quality); | |
float roulette_prob = 1.0 - (1.0 / inv_pdf); | |
// 光线被毙掉就不用继续了 | |
if (rand11() < roulette_prob) { | |
r.color *= roulette_prob; | |
break; | |
} | |
// 与地图求交 | |
record rec = raycast(r); | |
// 没击中物体就肯定击中天空 | |
if (!rec.hit) { | |
r.color *= sign(float(i)); // 纯黑色背景 | |
r.color *= sky(r, 2.0, 1.0 - 1.0 / (1.0 + 0.1 * float(i))); | |
break; | |
} | |
// 计算法线 | |
rec.normal = calc_normal(rec.obj, rec.pos); | |
// 更新光子的位置 | |
r.origin = rec.pos; | |
// 应用 PBR 材质更新光线 | |
r = BSDF(r, rec); | |
// 处理自发光 | |
float intensity = brightness(r.color); | |
r.color *= rec.obj.mtl.emission; | |
float visible = brightness(r.color); | |
// 光太暗或者击中光源 | |
if (intensity < visible || visible < VISIBILITY) break; | |
} | |
return r; | |
} | |
// 片段着色器程序入口 | |
vec4 fragment(vec2 uv, vec2 SCREEN_PIXEL_SIZE) { | |
// 计算摄像机方位和视线 | |
vec3 lookfrom = load(POSITION).xyz; | |
vec2 rotation = load(ROTATION).xy; | |
vec3 direction = CameraRotation(rotation) * vec3(0, 0, -1); | |
vec3 lookat = lookfrom + direction; | |
float aspect = SCREEN_PIXEL_SIZE.y / SCREEN_PIXEL_SIZE.x;; | |
// 初始化摄像机 | |
camera cam; | |
cam.lookfrom = lookfrom; | |
cam.lookat = lookat; | |
cam.aspect = aspect; | |
cam.vfov = camera_vfov; | |
cam.vup = vec3(0, 1, 0); | |
cam.focus = camera_focus; | |
cam.aperture = camera_aperture; | |
// 用 UV 和时间初始化随机数发生器种子 | |
seed = rand13(vec3(uv, iTime*iTimeDelta)); | |
// 超采样 | |
uv += rand21() * SCREEN_PIXEL_SIZE; | |
// 对每个光子经过的表面采样一次 | |
ray r = get_ray(cam, uv, vec3(1)); | |
// 处理颜色 | |
vec3 color = raytrace(r).color; | |
return vec4(color, 1.0); | |
} | |
void mainImage(out vec4 fragColor, in vec2 fragCoord) { | |
vec2 SCREEN_PIXEL_SIZE = 1.0 / iResolution.xy; | |
vec2 uv = fragCoord * SCREEN_PIXEL_SIZE; | |
fragColor = fragment(uv, SCREEN_PIXEL_SIZE); // 获取片元颜色 | |
if (bool(load(MOVING).x)) return; // 如果正在移动就不积累上一帧 | |
fragColor += texture(iChannel0, uv); // 积累帧进行降噪 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment