每日算法 — 使用java实现光线追踪:递归光线投射与BVH包围盒加速渲染

算法背景

光线追踪(Ray Tracing)是计算机图形学中最逼真的渲染技术之一。与光栅化渲染不同,光线追踪通过模拟光线在场景中的传播路径——包括反射、折射和阴影计算——来生成图像。该方法自Whitted于1980年提出以来,已成为电影特效、建筑可视化和现代实时游戏渲染的核心技术。

核心思想

光线追踪的基本思路是从相机位置向每个像素发射一条光线,计算光线与场景中物体的交点,然后根据材质属性决定光线是反射、折射还是吸收,并递归追踪后续光线,最终累积颜色值。

光线与球体求交

光线方程为 P(t) = O + t*D,其中 O 为原点,D 为方向向量,t > 0。球体方程为 |P - C|^2 = r^2。联立后得到关于 t 的一元二次方程:

|D|^2 * t^2 + 2*D*(O-C)*t + |O-C|^2 - r^2 = 0

通过判别式判断交点情况:
– Δ > 0:两个交点,取最小正根
– Δ = 0:相切
– Δ < 0:无交点

加速结构:BVH树

对于复杂场景,逐物体求交效率极低。包围体层次结构(Bounding Volume Hierarchy, BVH)通过递归地将场景分割为轴对齐包围盒(AABB),大幅减少求交次数。

Java完整实现

import java.util.*;

/**
 * 光线追踪渲染器:递归光线投射与BVH加速
 */
public class RayTracer {

    // 三维向量
    public static 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 multiply(double s) { return new Vec3(x * s, y * s, z * s); }
        public Vec3 multiply(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(dot(this)); }
        public Vec3 normalize() {
            double len = length();
            return len > 0 ? multiply(1.0 / len) : new Vec3(0, 0, 0);
        }
        public Vec3 reflect(Vec3 n) {
            return sub(n.multiply(2.0 * dot(n)));
        }

        @Override
        public String toString() {
            return String.format("(%.2f, %.2f, %.2f)", x, y, z);
        }
    }

    // 光线
    public static class Ray {
        public Vec3 origin, direction;

        public Ray(Vec3 origin, Vec3 direction) {
            this.origin = origin;
            this.direction = direction.normalize();
        }

        public Vec3 at(double t) {
            return origin.add(direction.multiply(t));
        }
    }

    // 材质类型
    public enum MaterialType { DIFFUSE, METAL, GLASS }

    // 材质
    public static class Material {
        public Vec3 albedo;           // 基础颜色
        public MaterialType type;
        public double fuzz;           // 金属模糊度
        public double refractionIndex; // 折射率

        public Material(Vec3 albedo, MaterialType type) {
            this.albedo = albedo;
            this.type = type;
            this.fuzz = 0.0;
            this.refractionIndex = 1.0;
        }
    }

    // 命中记录
    public static class HitRecord {
        public double t;
        public Vec3 point;
        public Vec3 normal;
        public Material material;
        public boolean frontFace;

        public void setFaceNormal(Ray ray, Vec3 outwardNormal) {
            frontFace = ray.direction.dot(outwardNormal) < 0;
            normal = frontFace ? outwardNormal : outwardNormal.multiply(-1.0);
        }
    }

    // 可求交物体接口
    public interface Hittable {
        boolean hit(Ray ray, double tMin, double tMax, HitRecord rec);
        Vec3 getMinBounds();
        Vec3 getMaxBounds();
    }

    // 球体
    public static class Sphere implements Hittable {
        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;
        }

        @Override
        public boolean hit(Ray ray, double tMin, double tMax, HitRecord rec) {
            Vec3 oc = ray.origin.sub(center);
            double a = ray.direction.dot(ray.direction);
            double halfB = oc.dot(ray.direction);
            double c = oc.dot(oc) - radius * radius;
            double discriminant = halfB * halfB - a * c;

            if (discriminant < 0) return false;

            double sqrtd = Math.sqrt(discriminant);
            double root = (-halfB - sqrtd) / a;

            if (root < tMin || root > tMax) {
                root = (-halfB + sqrtd) / a;
                if (root < tMin || root > tMax) return false;
            }

            rec.t = root;
            rec.point = ray.at(root);
            Vec3 outwardNormal = rec.point.sub(center).multiply(1.0 / radius);
            rec.setFaceNormal(ray, outwardNormal);
            rec.material = material;
            return true;
        }

        @Override
        public Vec3 getMinBounds() {
            return new Vec3(center.x - radius, center.y - radius, center.z - radius);
        }

        @Override
        public Vec3 getMaxBounds() {
            return new Vec3(center.x + radius, center.y + radius, center.z + radius);
        }
    }

    // BVH节点
    public static class BVHNode implements Hittable {
        public Hittable left, right;
        public Vec3 boxMin, boxMax;

        public BVHNode(List<Hittable> objects, int start, int end) {
            int axis = new Random().nextInt(3); // 随机选择分割轴
            objects.subList(start, end).sort((a, b) -> {
                double aMin = axis == 0 ? a.getMinBounds().x :
                              (axis == 1 ? a.getMinBounds().y : a.getMinBounds().z);
                double bMin = axis == 0 ? b.getMinBounds().x :
                              (axis == 1 ? b.getMinBounds().y : b.getMinBounds().z);
                return Double.compare(aMin, bMin);
            });

            int span = end - start;
            if (span == 1) {
                left = right = objects.get(start);
            } else if (span == 2) {
                left = objects.get(start);
                right = objects.get(start + 1);
            } else {
                int mid = start + span / 2;
                left = new BVHNode(objects, start, mid);
                right = new BVHNode(objects, mid, end);
            }

            boxMin = new Vec3(
                Math.min(left.getMinBounds().x, right.getMinBounds().x),
                Math.min(left.getMinBounds().y, right.getMinBounds().y),
                Math.min(left.getMinBounds().z, right.getMinBounds().z)
            );
            boxMax = new Vec3(
                Math.max(left.getMaxBounds().x, right.getMaxBounds().x),
                Math.max(left.getMaxBounds().y, right.getMaxBounds().y),
                Math.max(left.getMaxBounds().z, right.getMaxBounds().z)
            );
        }

        @Override
        public boolean hit(Ray ray, double tMin, double tMax, HitRecord rec) {
            if (!hitAABB(ray, tMin, tMax)) return false;

            boolean hitLeft = left.hit(ray, tMin, tMax, rec);
            boolean hitRight = right.hit(ray, tMin, hitLeft ? rec.t : tMax, rec);
            return hitLeft || hitRight;
        }

        // AABB包围盒求交
        private boolean hitAABB(Ray ray, double tMin, double tMax) {
            for (int a = 0; a < 3; a++) {
                double invD = 1.0 / (a == 0 ? ray.direction.x :
                                     (a == 1 ? ray.direction.y : ray.direction.z));
                double t0 = ((a == 0 ? boxMin.x : (a == 1 ? boxMin.y : boxMin.z))
                             - (a == 0 ? ray.origin.x : (a == 1 ? ray.origin.y : ray.origin.z))) * invD;
                double t1 = ((a == 0 ? boxMax.x : (a == 1 ? boxMax.y : boxMax.z))
                             - (a == 0 ? ray.origin.x : (a == 1 ? ray.origin.y : ray.origin.z))) * invD;
                if (invD < 0) { double tmp = t0; t0 = t1; t1 = tmp; }
                tMin = Math.max(t0, tMin);
                tMax = Math.min(t1, tMax);
                if (tMax <= tMin) return false;
            }
            return true;
        }

        @Override
        public Vec3 getMinBounds() { return boxMin; }

        @Override
        public Vec3 getMaxBounds() { return boxMax; }
    }

    // 折射计算(斯涅尔定律)
    public static Vec3 refract(Vec3 uv, Vec3 n, double etaiOverEtat) {
        double cosTheta = Math.min(uv.multiply(-1.0).dot(n), 1.0);
        Vec3 rOutPerp = uv.add(n.multiply(cosTheta)).multiply(etaiOverEtat);
        Vec3 rOutParallel = n.multiply(-Math.sqrt(Math.abs(1.0 - rOutPerp.dot(rOutPerp))));
        return rOutPerp.add(rOutParallel);
    }

    // 递归光线颜色计算
    public static Vec3 rayColor(Ray ray, Hittable world, int depth) {
        if (depth <= 0) return new Vec3(0, 0, 0);

        HitRecord rec = new HitRecord();
        if (!world.hit(ray, 0.001, Double.POSITIVE_INFINITY, rec)) {
            // 背景:天蓝色渐变
            double t = 0.5 * (ray.direction.normalize().y + 1.0);
            return new Vec3(1.0, 1.0, 1.0).multiply(1.0 - t)
                   .add(new Vec3(0.5, 0.7, 1.0).multiply(t));
        }

        Vec3 attenuation = new Vec3(0, 0, 0);
        Vec3 scatteredDir = new Vec3(0, 0, 0);
        Random rand = new Random();

        switch (rec.material.type) {
            case DIFFUSE:
                // 漫反射:随机散射
                Vec3 target = rec.point.add(rec.normal)
                              .add(randomUnitVector(rand));
                scatteredDir = target.sub(rec.point);
                attenuation = rec.material.albedo;
                break;
            case METAL:
                // 金属反射 + 模糊
                Vec3 reflected = ray.direction.normalize().reflect(rec.normal);
                scatteredDir = reflected.add(randomUnitVector(rand).multiply(rec.material.fuzz));
                attenuation = rec.material.albedo;
                if (scatteredDir.dot(rec.normal) <= 0)
                    return new Vec3(0, 0, 0);
                break;
            case GLASS:
                // 玻璃:折射或反射
                attenuation = new Vec3(1.0, 1.0, 1.0);
                double refractionRatio = rec.frontFace ?
                    (1.0 / rec.material.refractionIndex) : rec.material.refractionIndex;
                Vec3 unitDir = ray.direction.normalize();
                double cosTheta = Math.min(unitDir.multiply(-1.0).dot(rec.normal), 1.0);
                double sinTheta = Math.sqrt(1.0 - cosTheta * cosTheta);

                boolean cannotRefract = refractionRatio * sinTheta > 1.0;
                if (cannotRefract || reflectance(cosTheta, refractionRatio) > rand.nextDouble()) {
                    scatteredDir = unitDir.reflect(rec.normal);
                } else {
                    scatteredDir = refract(unitDir, rec.normal, refractionRatio);
                }
                break;
        }

        Ray scattered = new Ray(rec.point, scatteredDir);
        Vec3 color = rayColor(scattered, world, depth - 1);
        return new Vec3(attenuation.x * color.x,
                        attenuation.y * color.y,
                        attenuation.z * color.z);
    }

    // 生成单位球内随机向量
    public static Vec3 randomUnitVector(Random rand) {
        while (true) {
            Vec3 v = new Vec3(rand.nextDouble() * 2 - 1,
                              rand.nextDouble() * 2 - 1,
                              rand.nextDouble() * 2 - 1);
            if (v.dot(v) < 1.0) return v.normalize();
        }
    }

    // 菲涅尔反射近似
    public static double reflectance(double cosine, double refIdx) {
        double r0 = (1.0 - refIdx) / (1.0 + refIdx);
        r0 = r0 * r0;
        return r0 + (1.0 - r0) * Math.pow(1.0 - cosine, 5);
    }

    // 渲染场景(简化版,输出ASCII艺术)
    public static void renderAscii(Hittable world, int width, int height) {
        Vec3 cameraPos = new Vec3(0, 0, 0);
        double viewportHeight = 2.0;
        double viewportWidth = viewportHeight * ((double) width / height);
        double focalLength = 1.0;

        Vec3 horizontal = new Vec3(viewportWidth, 0, 0);
        Vec3 vertical = new Vec3(0, viewportHeight, 0);
        Vec3 lowerLeftCorner = cameraPos.sub(horizontal.multiply(0.5))
                                        .sub(vertical.multiply(0.5))
                                        .sub(new Vec3(0, 0, focalLength));

        String[] shades = {" ", ".", ":", "-", "=", "+", "*", "#", "%", "@"};

        System.out.println("光线追踪渲染结果(ASCII):");
        for (int j = height - 1; j >= 0; j--) {
            StringBuilder line = new StringBuilder();
            for (int i = 0; i < width; i++) {
                double u = (double) i / (width - 1);
                double v = (double) j / (height - 1);
                Ray ray = new Ray(cameraPos,
                    lowerLeftCorner.add(horizontal.multiply(u))
                                   .add(vertical.multiply(v))
                                   .sub(cameraPos));
                Vec3 color = rayColor(ray, world, 10);
                // 伽马校正
                double intensity = Math.sqrt(color.x) * 0.3 +
                                   Math.sqrt(color.y) * 0.59 +
                                   Math.sqrt(color.z) * 0.11;
                int shadeIdx = (int) Math.min(intensity * shades.length, shades.length - 1);
                line.append(shades[shadeIdx]);
            }
            System.out.println(line);
        }
    }

    public static void main(String[] args) {
        // 构建场景
        List<Hittable> objects = new ArrayList<>();

        // 地面
        objects.add(new Sphere(new Vec3(0, -100.5, -1), 100.0,
            new Material(new Vec3(0.8, 0.8, 0.0), MaterialType.DIFFUSE)));

        // 中心球(玻璃)
        Material glass = new Material(new Vec3(1.0, 1.0, 1.0), MaterialType.GLASS);
        glass.refractionIndex = 1.5;
        objects.add(new Sphere(new Vec3(0, 0, -1), 0.5, glass));

        // 左侧球(漫反射)
        objects.add(new Sphere(new Vec3(-1.0, 0, -1), 0.5,
            new Material(new Vec3(0.7, 0.3, 0.3), MaterialType.DIFFUSE)));

        // 右侧球(金属)
        Material metal = new Material(new Vec3(0.8, 0.8, 0.8), MaterialType.METAL);
        metal.fuzz = 0.3;
        objects.add(new Sphere(new Vec3(1.0, 0, -1), 0.5, metal));

        // 构建BVH加速结构
        Hittable world = new BVHNode(objects, 0, objects.size());

        // 渲染
        renderAscii(world, 60, 30);
    }
}

复杂度分析

场景 无加速 BVH加速
时间复杂度(求交) O(n) O(log n)
空间复杂度 O(n) O(n)
构建开销 无 O(n log n)

对于包含1000个物体的场景,BVH可将单像素求交次数从平均500次降低到约10次,加速比可达50倍以上。

关键技术点

  1. 递归深度控制:防止无限递归,通常限制在10-50层
  2. 浮点精度处理:使用 t > 0.001 避免自相交问题
  3. 伽马校正:将线性颜色空间转换为感知均匀空间
  4. 抗锯齿:每像素发射多条随机偏移光线取平均

扩展方向

  • 路径追踪:蒙特卡洛积分实现全局光照
  • HDR环境贴图:IBL实现真实环境反射
  • 纹理映射:UV坐标采样位图纹理
  • 运动模糊:时间维度采样实现动态模糊效果

总结

光线追踪通过物理模拟光线的传播行为,实现了照片级的渲染质量。BVH加速结构将求交复杂度从线性降低到对数级,使光线追踪在大规模场景中变得实用。掌握光线追踪原理,是理解现代GPU光追管线(如NVIDIA RTX)的基础。