First commit: Basic logic and implementation.

master
ternaryop8479 2026-01-25 10:20:42 +08:00
parent 0d946a00cb
commit ace62a1854
1 changed files with 0 additions and 463 deletions

View File

@ -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öllerTrumbore 三角形求交
返回 (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()