概述
光线追踪(Ray Tracing)是计算机图形学中最逼真的渲染技术之一,通过模拟光线在场景中的传播路径来生成图像。本文用Java实现一个基础的光线追踪器,重点讲解光线与球体的相交检测、基于反射定律的镜面反射,以及斯涅尔定律的折射模拟。仅需数百行代码,即可渲染出带有阴影、反射和折射效果的逼真场景。
核心算法设计
1. 三维向量运算基础
光线追踪的几乎所有计算都建立在三维向量运算之上。
public class Vec3 {
public double x, y, z;
public Vec3(double x, double y, double z) {
this.x = x; this.y = y; this.z = z;
}
public Vec3 add(Vec3 v) { return new Vec3(x + v.x, y + v.y, z + v.z); }
public Vec3 sub(Vec3 v) { return new Vec3(x - v.x, y - v.y, z - v.z); }
public Vec3 mul(double s) { return new Vec3(x * s, y * s, z * s); }
public Vec3 mul(Vec3 v) { return new Vec3(x * v.x, y * v.y, z * v.z); }
public double dot(Vec3 v) { return x * v.x + y * v.y + z * v.z; }
public Vec3 cross(Vec3 v) {
return new Vec3(
y * v.z - z * v.y,
z * v.x - x * v.z,
x * v.y - y * v.x
);
}
public double length() { return Math.sqrt(x*x + y*y + z*z); }
public Vec3 normalize() {
double len = length();
return len > 0 ? new Vec3(x/len, y/len, z/len) : new Vec3(0,0,0);
}
public static Vec3 reflect(Vec3 incident, Vec3 normal) {
// R = I - 2(I·N)N
return incident.sub(normal.mul(2 * incident.dot(normal)));
}
/**
* 斯涅尔定律计算折射方向
* @param etai 入射介质折射率
* @param etat 折射介质折射率
*/
public static Vec3 refract(Vec3 incident, Vec3 normal, double etai, double etat) {
double cosi = Math.max(-1, Math.min(1, incident.dot(normal)));
Vec3 n = normal;
if (cosi < 0) { cosi = -cosi; } else {
n = normal.mul(-1);
double tmp = etai; etai = etat; etat = tmp;
}
double eta = etai / etat;
double k = 1 - eta * eta * (1 - cosi * cosi);
if (k < 0) return new Vec3(0,0,0); // 全内反射
return incident.mul(eta).add(n.mul(eta * cosi - Math.sqrt(k)));
}
}
2. 光线表示与球体相交检测
光线用起点和方向向量表示。光线与球体的相交检测通过求解二次方程实现。
public class Ray {
public Vec3 origin; // 光线起点
public Vec3 direction; // 方向向量(需归一化)
public Ray(Vec3 origin, Vec3 direction) {
this.origin = origin;
this.direction = direction.normalize();
}
/** 光线在参数t处的点:P(t) = O + tD */
public Vec3 at(double t) {
return origin.add(direction.mul(t));
}
}
public class Sphere {
public Vec3 center;
public double radius;
public Material material;
public Sphere(Vec3 center, double radius, Material material) {
this.center = center;
this.radius = radius;
this.material = material;
}
/**
* 光线与球体相交检测
* 方程:|O + tD - C|² = r²
* 展开为关于t的二次方程:at² + bt + c = 0
*/
public boolean intersect(Ray ray, double[] tOut) {
Vec3 oc = ray.origin.sub(center);
double a = ray.direction.dot(ray.direction);
double b = 2.0 * oc.dot(ray.direction);
double c = oc.dot(oc) - radius * radius;
double discriminant = b * b - 4 * a * c;
if (discriminant < 0) return false; // 无实根,不相交
double sqrtDisc = Math.sqrt(discriminant);
double t = (-b - sqrtDisc) / (2 * a); // 取近点
if (t < 0.001) {
t = (-b + sqrtDisc) / (2 * a); // 近点在射线后方,取远点
if (t < 0.001) return false;
}
tOut[0] = t;
return true;
}
}
3. 材质与散射模型
定义三种材质类型:漫反射(Lambertian)、金属镜面反射、电介质(玻璃折射)。
public abstract class Material {
public abstract boolean scatter(Ray rayIn, HitRecord rec, Vec3 attenuation, Ray scattered);
}
/** 漫反射材质(Lambertian) */
public class Lambertian extends Material {
public Vec3 albedo; // 反射率颜色
public Lambertian(Vec3 albedo) { this.albedo = albedo; }
@Override
public boolean scatter(Ray rayIn, HitRecord rec, Vec3 attenuation, Ray scattered) {
// 在法线半球内随机散射
Vec3 target = rec.point.add(rec.normal).add(randomInUnitSphere());
scattered.origin = rec.point;
scattered.direction = target.sub(rec.point).normalize();
attenuation.x = albedo.x; attenuation.y = albedo.y; attenuation.z = albedo.z;
return true;
}
private Vec3 randomInUnitSphere() {
while (true) {
Vec3 p = new Vec3(Math.random()*2-1, Math.random()*2-1, Math.random()*2-1);
if (p.length() < 1) return p;
}
}
}
/** 金属材质(镜面反射) */
public class Metal extends Material {
public Vec3 albedo;
public double fuzz; // 模糊度(0=完美镜面)
public Metal(Vec3 albedo, double fuzz) {
this.albedo = albedo;
this.fuzz = Math.min(fuzz, 1.0);
}
@Override
public boolean scatter(Ray rayIn, HitRecord rec, Vec3 attenuation, Ray scattered) {
Vec3 reflected = Vec3.reflect(rayIn.direction, rec.normal);
scattered.origin = rec.point;
scattered.direction = reflected.add(randomInUnitSphere().mul(fuzz)).normalize();
attenuation.x = albedo.x; attenuation.y = albedo.y; attenuation.z = albedo.z;
return scattered.direction.dot(rec.normal) > 0; // 反射方向必须在法线同侧
}
}
/** 电介质材质(玻璃折射) */
public class Dielectric extends Material {
public double ir; // 折射率
public Dielectric(double ir) { this.ir = ir; }
@Override
public boolean scatter(Ray rayIn, HitRecord rec, Vec3 attenuation, Ray scattered) {
attenuation.x = 1.0; attenuation.y = 1.0; attenuation.z = 1.0;
double refractionRatio = rec.frontFace ? (1.0 / ir) : ir;
Vec3 unitDir = rayIn.direction.normalize();
double cosTheta = Math.min(unitDir.mul(-1).dot(rec.normal), 1.0);
double sinTheta = Math.sqrt(1.0 - cosTheta * cosTheta);
boolean cannotRefract = refractionRatio * sinTheta > 1.0;
Vec3 direction;
if (cannotRefract || reflectance(cosTheta, refractionRatio) > Math.random()) {
direction = Vec3.reflect(unitDir, rec.normal); // 反射
} else {
direction = Vec3.refract(unitDir, rec.normal, 1.0, refractionRatio); // 折射
}
scattered.origin = rec.point;
scattered.direction = direction.normalize();
return true;
}
/** 菲涅尔反射近似(Schlick近似) */
private double reflectance(double cosine, double refIdx) {
double r0 = (1 - refIdx) / (1 + refIdx);
r0 = r0 * r0;
return r0 + (1 - r0) * Math.pow(1 - cosine, 5);
}
}
4. 场景与光线追踪核心循环
public class Scene {
private List<Sphere> objects = new ArrayList<>();
public void add(Sphere s) { objects.add(s); }
/**
* 遍历场景所有物体,找到光线击中的最近物体
*/
public boolean hit(Ray ray, double tMin, double tMax, HitRecord rec) {
HitRecord tempRec = new HitRecord();
boolean hitAnything = false;
double closest = tMax;
for (Sphere obj : objects) {
double[] t = new double[1];
if (obj.intersect(ray, t) && t[0] < closest && t[0] > tMin) {
hitAnything = true;
closest = t[0];
tempRec.t = t[0];
tempRec.point = ray.at(t[0]);
tempRec.normal = tempRec.point.sub(obj.center).normalize();
tempRec.frontFace = ray.direction.dot(tempRec.normal) < 0;
if (!tempRec.frontFace) tempRec.normal = tempRec.normal.mul(-1);
tempRec.material = obj.material;
}
}
if (hitAnything) {
rec.copyFrom(tempRec);
}
return hitAnything;
}
}
public class RayTracer {
private Scene scene;
private Vec3 background = new Vec3(0.5, 0.7, 1.0); // 天空渐变底色
private int maxDepth = 50; // 最大递归深度
/**
* 递归计算光线颜色
* 光线击中物体 -> 根据材质散射产生新光线 -> 递归追踪
*/
public Vec3 trace(Ray ray, int depth) {
if (depth <= 0) return new Vec3(0, 0, 0);
HitRecord rec = new HitRecord();
if (scene.hit(ray, 0.001, Double.MAX_VALUE, rec)) {
Ray scattered = new Ray(new Vec3(0,0,0), new Vec3(0,0,0));
Vec3 attenuation = new Vec3(0,0,0);
if (rec.material.scatter(ray, rec, attenuation, scattered)) {
Vec3 color = trace(scattered, depth - 1);
return new Vec3(attenuation.x * color.x,
attenuation.y * color.y,
attenuation.z * color.z);
}
return new Vec3(0, 0, 0);
}
// 未击中任何物体,返回天空背景色(线性插值)
double t = 0.5 * (ray.direction.normalize().y + 1.0);
Vec3 white = new Vec3(1.0, 1.0, 1.0);
Vec3 blue = new Vec3(0.5, 0.7, 1.0);
return white.mul(1.0 - t).add(blue.mul(t));
}
}
5. 相机与抗锯齿
相机定义了观察视角,抗锯齿通过在每个像素内采样多条射线取平均实现。
public class Camera {
public Vec3 origin;
public Vec3 lowerLeftCorner;
public Vec3 horizontal;
public Vec3 vertical;
public Camera(Vec3 lookFrom, Vec3 lookAt, Vec3 vup,
double vfov, double aspectRatio) {
double theta = Math.toRadians(vfov);
double h = Math.tan(theta / 2);
double viewportHeight = 2.0 * h;
double viewportWidth = aspectRatio * viewportHeight;
Vec3 w = lookFrom.sub(lookAt).normalize();
Vec3 u = vup.cross(w).normalize();
Vec3 v = w.cross(u);
origin = lookFrom;
horizontal = u.mul(viewportWidth);
vertical = v.mul(viewportHeight);
lowerLeftCorner = origin.sub(horizontal.mul(0.5))
.sub(vertical.mul(0.5))
.sub(w);
}
/** 根据归一化屏幕坐标生成射线 */
public Ray getRay(double s, double t) {
Vec3 dir = lowerLeftCorner.add(horizontal.mul(s))
.add(vertical.mul(t))
.sub(origin);
return new Ray(origin, dir.normalize());
}
}
public class Renderer {
private RayTracer tracer;
private Camera camera;
private int samplesPerPixel = 100; // 每像素采样数(抗锯齿)
public void render(int width, int height, String filename) throws Exception {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
for (int j = height - 1; j >= 0; j--) {
for (int i = 0; i < width; i++) {
Vec3 color = new Vec3(0, 0, 0);
// 超采样抗锯齿:在像素内随机偏移采样
for (int s = 0; s < samplesPerPixel; s++) {
double u = (i + Math.random()) / (width - 1);
double v = (j + Math.random()) / (height - 1);
Ray ray = camera.getRay(u, v);
color = color.add(tracer.trace(ray, 50));
}
color = color.mul(1.0 / samplesPerPixel);
// Gamma校正
color = new Vec3(Math.sqrt(color.x), Math.sqrt(color.y), Math.sqrt(color.z));
image.setRGB(i, height - 1 - j, toRgb(color));
}
}
ImageIO.write(image, "png", new File(filename));
}
private int toRgb(Vec3 c) {
int r = (int)(255.999 * Math.min(1, Math.max(0, c.x)));
int g = (int)(255.999 * Math.min(1, Math.max(0, c.y)));
int b = (int)(255.999 * Math.min(1, Math.max(0, c.z)));
return (r << 16) | (g << 8) | b;
}
}
6. 完整场景构建示例
public class RayTracingMain {
public static void main(String[] args) throws Exception {
Scene scene = new Scene();
// 地面:巨大的漫反射球
scene.add(new Sphere(new Vec3(0, -100.5, -1), 100,
new Lambertian(new Vec3(0.8, 0.8, 0.0))));
// 中心:玻璃球(折射率1.5)
scene.add(new Sphere(new Vec3(0, 0, -1), 0.5,
new Dielectric(1.5)));
// 左侧:漫反射球
scene.add(new Sphere(new Vec3(-1, 0, -1), 0.5,
new Lambertian(new Vec3(0.4, 0.2, 0.1))));
// 右侧:金属球(轻微模糊)
scene.add(new Sphere(new Vec3(1, 0, -1), 0.5,
new Metal(new Vec3(0.8, 0.6, 0.2), 0.3)));
Camera cam = new Camera(
new Vec3(-2, 2, 1), // 相机位置
new Vec3(0, 0, -1), // 看向原点
new Vec3(0, 1, 0), // 上向量
20, // 垂直视场角
16.0 / 9.0 // 宽高比
);
RayTracer tracer = new RayTracer();
tracer.setScene(scene);
Renderer renderer = new Renderer(tracer, cam);
renderer.render(800, 450, "raytraced.png");
}
}
复杂度分析
| 算法模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 光线-球体相交 | O(1) | O(1) | 求解二次方程 |
| 单像素追踪 | O(S×D×N) | O(D) | S=采样数, D=递归深度, N=物体数 |
| 整幅图像渲染 | O(W×H×S×D×N) | O(W×H) | W=宽, H=高 |
| 菲涅尔反射 | O(1) | O(1) | Schlick近似 |
总结
光线追踪的核心思想是逆向模拟光线传播:从相机发射射线,追踪其在场景中的反射和折射路径。虽然基础实现的时间复杂度较高,但其物理正确性远超传统光栅化渲染。球体相交检测的二次方程求解、反射向量的镜像公式、以及斯涅尔定律的折射方向计算,构成了光线追踪算法的三大数学支柱。对于Java开发者,本项目是深入理解3D图形学和向量运算的绝佳实践。