You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从零实现光线追踪:3D渲染器隐藏面消除问题求助

3D渲染器隐藏面消除问题与光线追踪实现疑问

我开发了一款可解析ASCII格式.obj文件并投影至2D平面的3D渲染器,目前遇到的问题是:模型背面的不可见多边形会被渲染出来。查资料后知道这是**隐藏面消除(Hidden-surface determination)**问题,了解到可以用Z-Buffer或光线追踪解决。因为后续要添加光源,我倾向选择光线追踪,但对原理有疑问:从相机向屏幕每个像素发射射线时,怎么判断多边形可见性才不会导致形状失真或性能问题?

附上我的Processing代码:

// 12.11.2022
// Siehe Rotation Matrix in Wikipedia

// View Space: The world space vertex positions relative to the view of the camera

/* Die Verdeckungsberechnung ist zum korrekten Rendern einer 3D-Szene notwendig, weil Oberflächen,
   die für den Betrachter nicht sichtbar sind, auch nicht dargestellt werden sollten
*/

// ->  https://de.wikipedia.org/wiki/Sichtbarkeitsproblem


// TODO: Raytracing/Verdeckungsberechnung
// TODO: Texture Mapping

import java.util.Arrays;

import java.awt.Robot;


import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.ArrayList;

byte b[];

int amount = 0;
String lines[];


PVector[][] vertices;
int[] faces;



float a = 0;


PVector cam, cam_angle, cam_move, cam_speed;

float angle = 0.0;


void setup() {
  size(800,600);
  frameRate(60);
  noCursor();

  cam  = new PVector(0, 100, -500);
  cam_angle = new PVector(0, 0, 0);
  cam_move = new PVector(0, 0, 0);
  cam_speed = new PVector(50, 50, 50);
  
  lines =  loadStrings("UM2_SkullPile28mm.obj"); 
 
  println("File loaded. Now scanning contents...");
  
  println();
  
  Pattern numbers = Pattern.compile("(-?\\d+)");
  
  
  ArrayList<PVector> vertices_ = new ArrayList<PVector>();
  ArrayList<ArrayList> faces_ = new ArrayList<ArrayList>();
  
  int parsed_lines = 0; 
  
  for(String i:lines) {
    switch(i.charAt(0)) {
      
      // Find faces
      case 'f':
        ArrayList<Integer> values = new ArrayList<Integer>();
        for(Matcher m = numbers.matcher(i); m.find(); values.add(Integer.parseInt(m.group())));
        faces_.add(values);
        break;
        
      // Find Vectors  
      case 'v':
        String s[] = i.trim().split("\\s+");
        vertices_.add(new PVector(Float.parseFloat(s[1])*20, Float.parseFloat(s[2])*20, Float.parseFloat(s[3])*20));
        break;
    };
    if(++parsed_lines % (lines.length/6) == 0 || parsed_lines == lines.length) println((int)(map(parsed_lines, 0, lines.length, 0, 100)), "%");
  }
  
  println();
  println("Done. Found", vertices_.size(), "Vertices and", faces_.size(), "faces");
  
  

  int i=0;  
  
  vertices = new PVector[faces_.size()][];
  
  for(ArrayList<Integer> f_:faces_) {   
    vertices[i] = new PVector[f_.size()]; 
    int j = 0;
    
    for(int f: f_) {
      PVector v = vertices_.get(f-1);
      vertices[i][j] = Rotate3d_x(v, -90); 
      j++;
    }
    
    i++;
  }
  
}


PVector Rotate2d(PVector p, float a) {
  // a = angle

  float[][] m2 = {
    {cos(a), -sin(a)},
    {sin(a), cos(a)}
  };

  float[][] rotated = matmul(m2, new float[][] {
    { p.x },
    { p.y }
    });

  return new PVector(rotated[0][0], rotated[1][0]);
}


PVector Rotate3d(PVector p, float[][] m2) {

  float[][] rotated = matmul(m2, new float[][] {
    { p.x },
    { p.y },
    { p.z }
    });

  return new PVector(rotated[0][0], rotated[1][0], rotated[2][0]);
}



PVector Rotate3d_x(PVector p, float a) {
  return Rotate3d(p,
    new float[][] {
    {1, 0, 0},
    {0, cos(a), -sin(a)},
    {0, sin(a), cos(a)}
    });
};

PVector Rotate3d_y(PVector p, float a) {
  return Rotate3d(p,
    new float[][] {
    {cos(a), 0, sin(a)},
    {0, 1, 0},
    {-sin(a), 0, cos(a)}
    });
}

PVector Rotate3d_z(PVector p, float a) {
  return Rotate3d(p,
    new float[][] {
    {cos(a), -sin(a), 0},
    {sin(a), cos(a), 0},
    {0, 0, 1}
    });
}

PVector Rotate3d(PVector p, PVector a) {
  return Rotate3d_z( Rotate3d_y(Rotate3d_x(p, a.x), a.y), a.z );
}



// Matrixmultiplikation
float[][] matmul(float[][] m1, float[][] m2) {

  int cols_m1 = m1.length,
    rows_m1 = m1[0].length;

  int cols_m2 = m2.length,
    rows_m2 = m2[0].length;

  try {
    if (rows_m1 != cols_m2) throw new Exception("Rows of m1 must match Columns of m2!");
  }
  catch(Exception e) {
    println(e);
  }


  float[][] res = new float[cols_m2][rows_m2];


  for (int c=0; c < cols_m1; c++) {

    for (int r2=0; r2 < rows_m2; r2++) {

      float sum = 0;
      float[] buf = new float[rows_m1];

      // Multiply rows of m1 with columns of m2 and store in buf
      for (int r=0; r < rows_m1; r++) {
        buf[r] = m1[c][r]* m2[r][r2];
      }

      // Add up all entries into sum
      for (float entry : buf) {
        sum += entry;
      }

      res[c][r2] = sum;
    }
  }

  return res;
}


PVector applyPerspective(PVector p) {
    PVector d = applyViewTransform(p);
    return applyPerspectiveTransform(d);
}

PVector applyViewTransform(PVector p) {
    // c = camera position
    // co = camera orientation / camera rotation
    PVector c = cam;
    PVector co = cam_angle;
 

    // dx, dy, dz     https://en.wikipedia.org/wiki/3D_projection   :   Mathematical Formula
    float[][] dxyz = matmul(
            matmul(new float[][]{ 
                    {1, 0, 0},
                    {0, cos(co.x), sin(co.x)},
                    {0, -sin(co.x), cos(co.x)}
            }, new float[][]{ 
                    {cos(co.y), 0, -sin(co.y)},
                    {0, 1, 0},
                    {sin(co.y), 0, cos(co.y)}
            }),

            matmul(new float[][]{ 
                    {cos(co.z), sin(co.z), 0},
                    {-sin(co.z), cos(co.z), 0},
                    {0, 0, 1}
            }, new float[][]{ 
                    {p.x - c.x},
                    {p.y - c.y},
                    {p.z - c.z},
            }));

    PVector d = new PVector(dxyz[0][0], dxyz[1][0], dxyz[2][0]);
    return d;
}


PVector applyPerspectiveTransform(PVector d) {
     // e = displays surface pos relative to camera pinhole c
    PVector e = new PVector(0, 0, 300);
    return new PVector((e.z / d.z) * d.x + e.x, (e.z / d.z) * d.y + e.y);
}


void draw() {
  background(255);
  translate(width/2, height/2);
  scale(1,-1); 
  noStroke();
  fill(0, 100, 0, 50);
  
 
  
  PVector[][] points_view = new PVector[vertices.length][];
  
  for(int i=0; i < vertices.length; i++) {
    points_view[i] = new PVector[vertices[i].length];
    for(int j=0; j < vertices[i].length; j++)
    points_view[i][j] = applyViewTransform(Rotate3d_y(vertices[i][j], angle));
  }


    // The following snippet I got from:  https://stackoverflow.com/questions/74443149/3d-projection-axis-inversion-problem-java-processing?noredirect=1#comment131433616_74443149
  
    float nearPlane = 1.0;   
    
    for (int c = 0; c < points_view.length; c++) {
      beginShape();
      
        for (int r = 0; r < points_view[c].length-1; r++) {
            // Alle Punkte verbinden
            //if (i == a) continue;
            
            PVector p0 = points_view[c][r];    
            PVector p1 = points_view[c][r+1];
             
            
             if(p0.z < nearPlane && p1.z < nearPlane){ continue; };
             
           
             if(p0.z >= nearPlane && p1.z < nearPlane)
             p1 = PVector.lerp(p0, p1, (p0.z - nearPlane) / (p0.z - p1.z));
              
             if(p0.z < nearPlane && p1.z >= nearPlane)
             p0 = PVector.lerp(p1, p0, (p1.z - nearPlane) / (p1.z - p0.z));
 
            
            // project
            p0 = applyPerspectiveTransform(p0);
            p1 = applyPerspectiveTransform(p1);
            vertex(p0.x, p0.y);
            vertex(p1.x, p1.y);

        }
        endShape();
    }
    
  


  
}

光线追踪实现隐藏面消除的核心思路与优化方案

1. 可见性判断逻辑

对每个像素的射线,执行以下步骤:

  • 射线生成:以相机位置为起点,根据像素在屏幕上的坐标计算射线方向(需结合相机的朝向和投影参数)
  • 相交检测:遍历场景中所有多边形,计算射线与多边形的交点:
    1. 先判断射线是否与多边形所在平面相交,计算交点的3D坐标
    2. 用重心坐标法或射线交叉计数法,验证交点是否落在多边形内部
  • 最近交点筛选:记录所有有效交点中距离相机最近的那个,对应的多边形就是该像素可见的面,直接渲染该面的颜色即可

2. 避免形状失真的关键

  • 射线方向精度:确保射线方向计算时考虑相机的旋转矩阵,精确映射屏幕像素到3D空间方向
  • 相交检测精度:使用浮点运算时加入微小的epsilon(比如1e-6),避免因精度误差导致的交点误判
  • 多边形拓扑正确性:确保.obj文件解析时正确读取多边形的顶点顺序,法向量计算准确

3. 性能优化建议

  • 背面剔除:预先计算每个多边形的法向量,若法向量与射线方向的点积大于0(多边形背向相机),直接跳过该多边形的相交检测
  • 空间划分:用BVH(边界体积层次)将场景多边形分组,射线先与包围盒相交,再检测盒内的多边形,减少检测数量
  • 分层次渲染:先渲染低分辨率图像,再对边缘像素做抗锯齿采样,平衡性能与画质

针对代码的修改提示

第一步:添加多边形法向量计算函数

// 计算多边形的法向量,顶点需按顺时针或逆时针顺序排列
PVector calculateFaceNormal(PVector[] faceVertices) {
  PVector edge1 = PVector.sub(faceVertices[1], faceVertices[0]);
  PVector edge2 = PVector.sub(faceVertices[2], faceVertices[0]);
  PVector normal = edge1.cross(edge2);
  normal.normalize();
  return normal;
}

第二步:替换渲染逻辑为光线追踪框架

在draw()函数中,替换原有的多边形遍历渲染,改为逐像素射线检测:

void draw() {
  background(255);
  loadPixels();

  float fov = PI/3; // 视场角
  float aspect = (float)width/height;

  for (int x = 0; x < width; x++) {
    for (int y = 0; y < height; y++) {
      // 将屏幕坐标转换为NDC(归一化设备坐标)
      float nx = (2.0f * x / width) - 1.0f;
      float ny = 1.0f - (2.0f * y / height);
      ny *= aspect;

      // 计算射线方向(考虑相机旋转)
      PVector rayDir = new PVector(nx * tan(fov/2), ny * tan(fov/2), 1);
      rayDir = Rotate3d(rayDir, cam_angle);
      rayDir.normalize();

      float closestDist = Float.MAX_VALUE;
      int pixelColor = color(255);

      // 遍历所有多边形
      for (int faceIdx = 0; faceIdx < vertices.length; faceIdx++) {
        PVector[] face = vertices[faceIdx];
        // 背面剔除:计算法向量与射线方向的点积
        PVector normal = calculateFaceNormal(face);
        if (PVector.dot(normal, rayDir) > 0) continue;

        // 射线与多边形相交检测(此处简化实现,需完善)
        float dist = rayIntersectsPolygon(cam, rayDir, face);
        if (dist > 0 && dist < closestDist) {
          closestDist = dist;
          pixelColor = color(0, 100, 0, 255); // 可见面的颜色
        }
      }

      pixels[y * width + x] = pixelColor;
    }
  }

  updatePixels();
  angle += 0.01;
}

// 射线与多边形相交检测的简化实现
float rayIntersectsPolygon(PVector rayOrigin, PVector rayDir, PVector[] polygon) {
  // 先计算射线与平面的交点
  PVector normal = calculateFaceNormal(polygon);
  float denom = PVector.dot(normal, rayDir);
  if (abs(denom) < 1e-6) return -1; // 射线与平面平行

  float t = PVector.dot(PVector.sub(polygon[0], rayOrigin), normal) / denom;
  if (t < 0) return -1; // 交点在相机后方

  PVector hitPoint = PVector.add(rayOrigin, PVector.mult(rayDir, t));

  // 验证交点是否在多边形内部(此处用重心坐标法简化)
  // 注:完整实现需处理任意边数的多边形,此处假设为三角形
  if (polygon.length == 3) {
    PVector v0 = polygon[0];
    PVector v1 = polygon[1];
    PVector v2 = polygon[2];

    PVector v0v1 = PVector.sub(v1, v0);
    PVector v0v2 = PVector.sub(v2, v0);
    PVector v0p = PVector.sub(hitPoint, v0);

    float dot00 = PVector.dot(v0v1, v0v1);
    float dot01 = PVector.dot(v0v1, v0v2);
    float dot02 = PVector.dot(v0v1, v0p);
    float dot11 = PVector.dot(v0v2, v0v2);
    float dot12 = PVector.dot(v0v2, v0p);

    float invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
    float u = (dot11 * dot02 - dot01 * dot12) * invDenom;
    float v = (dot00 * dot12 - dot01 * dot02) * invDenom;

    if (u >= 0 && v >= 0 && u + v <= 1) {
      return t;
    }
  }

  return -1;
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 05:10:31