diff --git a/experiments/rt.py b/experiments/rt.py deleted file mode 100644 index 9d4185e..0000000 --- a/experiments/rt.py +++ /dev/null @@ -1,463 +0,0 @@ -# -*- coding: utf-8 -*- -""" -简易三角形康奈尔盒渲染器(递归反射版本) - -特性: -- 只用三角形面片构建场景(墙面、灯、两个盒子) -- Camera 视口被视为一个平面(理论上可拆成两个三角形) -- 漫反射表面:不递归,直接光照 -- 镜面(金属)表面:递归反射,模拟金属反射 -- 输出 PPM 图片:cornell.ppm -""" - -import math - -# ====================== -# 基础向量运算 -# ====================== - -def v_add(a, b): - return (a[0] + b[0], a[1] + b[1], a[2] + b[2]) - -def v_sub(a, b): - return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) - -def v_mul(a, s): - # 向量 * 标量 - return (a[0] * s, a[1] * s, a[2] * s) - -def v_mul_comp(a, b): - # 分量乘(用于颜色调制) - return (a[0] * b[0], a[1] * b[1], a[2] * b[2]) - -def v_dot(a, b): - return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] - -def v_cross(a, b): - return ( - a[1]*b[2] - a[2]*b[1], - a[2]*b[0] - a[0]*b[2], - a[0]*b[1] - a[1]*b[0] - ) - -def v_length(a): - return math.sqrt(v_dot(a, a)) - -def v_norm(a): - l = v_length(a) - if l == 0: - return (0.0, 0.0, 0.0) - return (a[0]/l, a[1]/l, a[2]/l) - -def v_reflect(d, n): - # 反射方向:r = d - 2*(d·n)*n - dn = v_dot(d, n) - return v_sub(d, v_mul(n, 2.0 * dn)) - -# ====================== -# 材质和三角形定义 -# ====================== - -class Material: - def __init__(self, color=(1.0, 1.0, 1.0), - emission=(0.0, 0.0, 0.0), - kind="diffuse", - reflectivity=1.0): - """ - kind: "diffuse" 或 "mirror" - color: 基础颜色/反射率 - emission: 自发光颜色(光源) - reflectivity: 镜面反射强度(0~1) - """ - self.color = color - self.emission = emission - self.kind = kind - self.reflectivity = reflectivity - -class Triangle: - def __init__(self, v0, v1, v2, material): - self.v0 = v0 - self.v1 = v1 - self.v2 = v2 - self.material = material - # 预计算几何法线 - self.normal = v_norm(v_cross(v_sub(v1, v0), v_sub(v2, v0))) - - def intersect(self, ray_o, ray_d, eps=1e-6): - """ - Möller–Trumbore 三角形求交 - 返回 (t, u, v) 或 None - """ - v0, v1, v2 = self.v0, self.v1, self.v2 - edge1 = v_sub(v1, v0) - edge2 = v_sub(v2, v0) - h = v_cross(ray_d, edge2) - a = v_dot(edge1, h) - if -eps < a < eps: - return None # 平行 - f = 1.0 / a - s = v_sub(ray_o, v0) - u = f * v_dot(s, h) - if u < 0.0 or u > 1.0: - return None - q = v_cross(s, edge1) - v = f * v_dot(ray_d, q) - if v < 0.0 or u + v > 1.0: - return None - t = f * v_dot(edge2, q) - if t > eps: - return (t, u, v) - return None - -# ====================== -# 场景构建(康奈尔盒) -# ====================== - -def make_quad(v00, v10, v11, v01, material): - """ - 用两个三角形表示一个四边形: - v00----v10 - | / - | / - v01----v11 - """ - return [ - Triangle(v00, v10, v11, material), - Triangle(v00, v11, v01, material) - ] - -def add_box(x0, x1, y0, y1, z0, z1, material): - """ - 用 12 个三角形(6 个面 * 2)构造轴对齐长方体 - """ - tris = [] - # 8 个顶点 - p000 = (x0, y0, z0) - p001 = (x0, y0, z1) - p010 = (x0, y1, z0) - p011 = (x0, y1, z1) - p100 = (x1, y0, z0) - p101 = (x1, y0, z1) - p110 = (x1, y1, z0) - p111 = (x1, y1, z1) - - # +X 面 - tris += make_quad(p100, p110, p111, p101, material) - # -X 面 - tris += make_quad(p000, p001, p011, p010, material) - # +Y 面(顶面) - tris += make_quad(p010, p011, p111, p110, material) - # -Y 面(底面) - tris += make_quad(p000, p100, p101, p001, material) - # +Z 面 - tris += make_quad(p001, p101, p111, p011, material) - # -Z 面 - tris += make_quad(p000, p010, p110, p100, material) - - return tris - -def build_cornell_scene(): - """ - 构建一个 0~1 立方体空间内的简易康奈尔盒 - - 左墙红色,右墙绿色,其余白色 - - 顶部一个小矩形面光源 - - 场景中两个盒子:一个漫反射,一个金属镜面 - """ - scene = [] - - # 材质 - white = Material(color=(0.8, 0.8, 0.8), kind="diffuse") - red = Material(color=(0.75, 0.15, 0.15), kind="diffuse") - green = Material(color=(0.15, 0.75, 0.15), kind="diffuse") - light_mat = Material(color=(1.0, 1.0, 1.0), - emission=(12.0, 12.0, 12.0), # 强光 - kind="diffuse") - - metal = Material(color=(0.9, 0.9, 0.9), - kind="mirror", - reflectivity=0.95) - - grey_box = Material(color=(0.75, 0.75, 0.75), - kind="diffuse") - - # 盒子边界 - minc, maxc = 0.0, 1.0 - - # 地板 y=0 - scene += make_quad( - (minc, 0.0, minc), - (maxc, 0.0, minc), - (maxc, 0.0, maxc), - (minc, 0.0, maxc), - white - ) - - # 天花板 y=1 - scene += make_quad( - (minc, 1.0, minc), - (minc, 1.0, maxc), - (maxc, 1.0, maxc), - (maxc, 1.0, minc), - white - ) - - # 后墙 z=1 - scene += make_quad( - (minc, minc, 1.0), - (maxc, minc, 1.0), - (maxc, maxc, 1.0), - (minc, maxc, 1.0), - white - ) - - # 左墙 x=0 (红) - scene += make_quad( - (0.0, minc, minc), - (0.0, minc, maxc), - (0.0, maxc, maxc), - (0.0, maxc, minc), - red - ) - - # 右墙 x=1 (绿) - scene += make_quad( - (1.0, minc, minc), - (1.0, maxc, minc), - (1.0, maxc, maxc), - (1.0, minc, maxc), - green - ) - - # 顶部面光源(天花板中间一小块) - lx0, lx1 = 0.35, 0.65 - lz0, lz1 = 0.35, 0.65 - ly = 0.999 # 略低于天花板避免数值问题 - light_tris = make_quad( - (lx0, ly, lz0), - (lx1, ly, lz0), - (lx1, ly, lz1), - (lx0, ly, lz1), - light_mat - ) - scene += light_tris - - # 低盒子(金属反射) - scene += add_box( - 0.20, 0.50, # x - 0.0, 0.30, # y - 0.20, 0.50, # z - metal - ) - - # 高盒子(漫反射) - scene += add_box( - 0.55, 0.85, - 0.0, 0.70, - 0.55, 0.85, - grey_box - ) - - return scene - -# ====================== -# 光线追踪核心 -# ====================== - -def find_closest_hit(scene, ray_o, ray_d): - """ - 在所有三角形中找到最近交点 - 返回 (tri, t, hit_pos, hit_normal) 或 None - """ - closest_t = float('inf') - hit_tri = None - hit_pos = None - hit_normal = None - - for tri in scene: - res = tri.intersect(ray_o, ray_d) - if res is None: - continue - t, u, v = res - if t < closest_t: - closest_t = t - # 交点 - hit_pos = v_add(ray_o, v_mul(ray_d, t)) - # 几何法线拷贝一份 - n = tri.normal - # 确保法线朝向入射光线的反方向 - if v_dot(n, ray_d) > 0: - n = v_mul(n, -1.0) - hit_normal = n - hit_tri = tri - - if hit_tri is None: - return None - return hit_tri, closest_t, hit_pos, hit_normal - -def build_lights(scene): - """ - 从场景中提取所有发光三角形 - """ - lights = [] - for tri in scene: - if (tri.material.emission[0] > 0 or - tri.material.emission[1] > 0 or - tri.material.emission[2] > 0): - lights.append(tri) - return lights - -def triangle_center(tri): - return ( - (tri.v0[0] + tri.v1[0] + tri.v2[0]) / 3.0, - (tri.v0[1] + tri.v1[1] + tri.v2[1]) / 3.0, - (tri.v0[2] + tri.v1[2] + tri.v2[2]) / 3.0, - ) - -def trace_ray(scene, lights, ray_o, ray_d, depth, max_depth=4, eps=1e-4): - """ - 核心递归光线追踪函数 - - 只在 kind='mirror' 时递归 - - kind='diffuse' 时只做直接光照,不再递归 - """ - if depth > max_depth: - return (0.0, 0.0, 0.0) - - hit = find_closest_hit(scene, ray_o, ray_d) - if hit is None: - # 背景颜色(黑) - return (0.0, 0.0, 0.0) - - tri, t, hit_pos, n = hit - mat = tri.material - - # 自发光(光源),直接返回其发光颜色(主要用于第一次命中) - color = mat.emission - - # 漫反射:只做一次直接光照 - if mat.kind == "diffuse": - direct = (0.0, 0.0, 0.0) - for light in lights: - # 简化:只用光源三角形中心一个点做光照 & 阴影测试 - lp = triangle_center(light) - to_light = v_sub(lp, hit_pos) - dist2 = v_dot(to_light, to_light) - dist = math.sqrt(dist2) - l_dir = v_norm(to_light) - - # 阴影检测:从 hit_pos 向 lp 打一条 shadow ray - shadow_o = v_add(hit_pos, v_mul(n, eps)) - shadow_hit = find_closest_hit(scene, shadow_o, l_dir) - blocked = False - if shadow_hit is not None: - _, t_shadow, _, _ = shadow_hit - if t_shadow < dist - eps: - blocked = True - - if not blocked: - # 漫反射 Lambert 项 - lambert = max(0.0, v_dot(n, l_dir)) - if lambert > 0: - # 简单地把光源发光乘以漫反射颜色和 lambert - # 不严格做 1/r^2 衰减,只做一个系数缩放 - # 可以适当乘个缩放系数让亮度合适 - intensity = 1.0 - # contribution = mat.color * light.emission * lambert * intensity - c = v_mul_comp(mat.color, light.material.emission) - c = v_mul(c, lambert * intensity) - direct = v_add(direct, c) - - color = v_add(color, direct) - return color - - # 镜面(金属)表面:计算反射并递归 - if mat.kind == "mirror": - refl_dir = v_reflect(ray_d, n) - refl_o = v_add(hit_pos, v_mul(n, eps)) - refl_col = trace_ray(scene, lights, refl_o, refl_dir, depth + 1, max_depth, eps) - # 反射颜色 = 本身颜色 * 反射颜色 * 反射率 - refl_col = v_mul_comp(mat.color, refl_col) - refl_col = v_mul(refl_col, mat.reflectivity) - color = v_add(color, refl_col) - return color - - # 其他类型(未用到),直接返回自发光 - return color - -# ====================== -# 渲染主函数 -# ====================== - -def clamp(x, lo=0.0, hi=1.0): - return max(lo, min(hi, x)) - -def gamma_correct(c, gamma=2.2): - inv = 1.0 / gamma - return (c[0] ** inv, c[1] ** inv, c[2] ** inv) - -def render(): - # 图像分辨率 - width = 400 - height = 400 - - # 构建场景 - scene = build_cornell_scene() - lights = build_lights(scene) - - # 相机设置 - cam_pos = (0.5, 0.5, -1.5) # 在盒子前方 - look_at = (0.5, 0.5, 0.0) # 看向盒子中心 - up = (0.0, 1.0, 0.0) - fov = 40.0 # 视角(度) - aspect = width / float(height) - scale = math.tan(math.radians(fov * 0.5)) - - # 构建相机坐标系 - forward = v_norm(v_sub(look_at, cam_pos)) - right = v_norm(v_cross(forward, up)) - true_up = v_cross(right, forward) - - framebuffer = [[(0.0, 0.0, 0.0) for _ in range(width)] for _ in range(height)] - - for y in range(height): - print(f"Rendering line {y+1}/{height} ...", end="\r") - for x in range(width): - # 像素中心 NDC 坐标 [-1,1] - px = (2.0 * ((x + 0.5) / width) - 1.0) * aspect * scale - py = (1.0 - 2.0 * ((y + 0.5) / height)) * scale - - # 在相机坐标系中构造方向 - dir_cam = v_add( - v_add(v_mul(right, px), v_mul(true_up, py)), - forward - ) - ray_d = v_norm(dir_cam) - ray_o = cam_pos - - col = trace_ray(scene, lights, ray_o, ray_d, depth=0, max_depth=4) - - # gamma 校正 - col = gamma_correct(( - clamp(col[0]), - clamp(col[1]), - clamp(col[2]) - )) - - framebuffer[y][x] = col - - print("\n渲染完成,正在写入文件 cornell.ppm ...") - - # 输出 PPM(文本格式) - with open("cornell.ppm", "w") as f: - f.write(f"P3\n{width} {height}\n255\n") - for y in range(height): - for x in range(width): - r, g, b = framebuffer[y][x] - ir = int(255.99 * clamp(r)) - ig = int(255.99 * clamp(g)) - ib = int(255.99 * clamp(b)) - f.write(f"{ir} {ig} {ib}\n") - - print("写入完成:cornell.ppm") - -if __name__ == "__main__": - render()