如何隐藏 3d 绘图的不可见元素?

Posted

技术标签:

【中文标题】如何隐藏 3d 绘图的不可见元素?【英文标题】:How to hide invisible elements of the 3d drawing? 【发布时间】:2021-04-04 04:38:01 【问题描述】:

我正在尝试绘制显示波纹的 3d 图像:

function myFunc(x, y) 
  let zRipple =
    Math.pow(2, -0.005 * (Math.abs(x) + Math.abs(y))) *
    Math.cos(((x * x + y * y) * 2 * pi) / 180 / width) *
    height;

  return zRipple;

此处的宽度和高度是定义绘图区域的常量,在我的测试中等于 200。

我的方法基于我从 30 年前读过的一篇文章中回忆的内容,现在我试图回忆起来。

这个想法是:

将整个绘图板分割成 10 像素的网格

对于网格的每个“单元格”,沿 Y 轴和 X 轴画一条线到最近的单元格”(步长=10,ds=0.0

for (let x3 = width; x3 >= - width; x3 -= step) 
  for (let y3 = -height; y3 <= height; y3 += step) 
    for (let s = 0; s < step; s += ds) 
      let x = x3 + s;
        if (x < width) 
          let z3 = myFunc(x, y3);
          drawPixel3d(x, y3, z3);
        
      

      for (let s = 0; s < step; s += ds) 
        let y = y3 + s;
        if (y < height) 
          let z3 = myFunc(x3, y);
          drawPixel3d(x3, y, z3);
        
      
    
  

这是我将 3d 坐标转换为 2d 的方法:

function drawPixel3d(x3, y3, z3) 
  let x2 = (x3 + y3) * Math.sin((60 * pi) / 180);
  let y2 = z3 - ((x3 - y3) * Math.sin((30 * pi) / 180)) / 4;
  drawPixel(x2, y2);

如下图所示,我得到了一个不错的图形,但有一个问题:我绘制了所有的点,而不仅仅是那些,可见

问题:如何检查是否需要显示任何像素?

根据我在那篇文章中的记忆,我们应该遵循以下方法:

从场景的前部开始绘制(我相信我会这样做,如果带有坐标(宽度,-高度)的点,则最接近观察者或屏幕 对于每个像素列 - 记住“Z”坐标,并且仅在其 Z 坐标大于上次记录的像素时才绘制新像素

为了实现这一点,我修改了我的“drawPixel3d”方法:

function drawPixel3d(x3, y3, z3) 
  let x2 = (x3 + y3) * Math.sin((60 * pi) / 180);
  let y2 = z3 - ((x3 - y3) * Math.sin((30 * pi) / 180)) / 4;

  let n = Math.round(x2);
  let visible = false;
  if (zs[n] === undefined) 
    zs[n] = z3;
    visible = true;
   else 
    if (z3 > zs[n]) 
      visible = z3 > zs[n];
      zs[n] = z3;
    
  

  if (visible) drawPixel(x2, y2);

但结果出乎意料:

我做错了什么?或者另一个问题:如何绘制简单的 3d 图形?

谢谢!

附:程序的最后一段(在实际绘图过程中说明了 Y 坐标的反转):

function drawPixel(x: number, y: number) 
  ctx.fillRect(cX + x, cY - y, 1, 1); // TS-way to draw pixel on canvas is to draw a rectangle
   // cX and cY are coordinates of the center of the drawing canvas

P.P.S.我对算法解决方案有一个想法,所以添加了一个“算法”标签:也许这个社区的人可以提供帮助?

【问题讨论】:

【参考方案1】:

我得到了解决方案的想法:从离观察者最近的点开始绘制,但是对于 x2 和 y2 坐标的每个组合,只绘制一次像素,并且只在它可见时才绘制(从不绘制点在其他人之后)...唯一的问题是我没有绘制表面的每个点,我只绘制了一个 10 点步长的表面网格。因此,部分表面将在网格单元“之间”可见。

另一个想法是计算从表面的每个绘图点到观察者的距离,并确保只绘制表面上距离观察者最近的那个点......但是如何?

【讨论】:

【参考方案2】:

您的表面是凹面的,这意味着您不能使用基于面法线和相机视图方向之间点积的简单方法。

你有 3 个明显的选择。

    使用光线追踪

    当你得到表面的解析方程时,这可能是更好的方法

    使用深度缓冲来掩盖不可见的东西

    当您渲染线框时,您需要分 2 次执行此操作:

      渲染不可见的填充表面(仅填充深度缓冲区而不是屏幕) 渲染线框

    您的深度缓冲区条件也必须包含相等的值,因此 z&lt;=depth[y][x]z&gt;=depth[y][x]

    但是你需要使用面部渲染(三角形或四边形......),我认为这是软件渲染,所以如果你不熟悉这些东西,请参阅:

    how to rasterize rotated rectangle (in 2d by setpixel) Algorithm to fill triangle

    利用拓扑结构使用深度排序

    如果您没有视图转换,因此您的 x,y,z 坐标直接对应于相机空间坐标,那么您可以通过对 for 循环和方向进行排序以 从后到前 顺序渲染网格迭代(在 等距 视图中很常见)。这不需要深度缓冲,但是您需要渲染填充 QUADS 以获得正确的输出(边框设置为绘图颜色,内部填充背景颜色)。

我确实选择了 #2 方法。当我将最后一个链接移植到 3D 中时,我得到了这个(C++ 代码):

//---------------------------------------------------------------------------
const int col_transparent=-1;   // transparent color
class gfx_main
    
public:
    Graphics::TBitmap *bmp; // VCL bitmap for win32 rendering
    int **scr,**zed,xs,ys;  // screen,depth buffers and resolution
    struct pbuf             // convex polygon rasterization line buffer
        
        int x,z;            // values to interpolate during rendering
        pbuf()  
        pbuf(pbuf& a)    *this=a; 
        ~pbuf() 
        pbuf* operator = (const pbuf *a)  *this=*a; return this; 
        //pbuf* operator = (const pbuf &a)  ...copy... return this; 
         *pl,*pr;          // left,right buffers
    gfx_main();
    gfx_main(gfx_main& a)    *this=a; 
    ~gfx_main();
    gfx_main* operator = (const gfx_main *a)  *this=*a; return this; 
    //gfx_main* operator = (const gfx_main &a)  ...copy... return this; 
    void resize(int _xs=-1,int _ys=-1);
    void clear(int z,int col);              // clear buffers
    void pixel(int x,int y,int z,int col);  // render 3D point
    void line(int x0,int y0,int z0,int x1,int y1,int z1,int col); // render 3D line
    void triangle(int x0,int y0,int z0,int x1,int y1,int z1,int x2,int y2,int z2,int col); // render 3D triangle
    void _triangle_line(int x0,int y0,int z0,int x1,int y1,int z1); // this is just subroutine
    ;
//---------------------------------------------------------------------------
gfx_main::gfx_main()
    
    bmp=new Graphics::TBitmap;
    scr=NULL;
    zed=NULL;
    pl =NULL;
    pr =NULL;
    xs=0; ys=0;
    resize(1,1);
    
//---------------------------------------------------------------------------
gfx_main::~gfx_main()
    
    if (bmp) delete bmp;
    if (scr) delete[] scr;
    if (zed)
        
        if (zed[0]) delete[] zed[0];
        delete[] zed;
        
    if (pl) delete[] pl;
    if (pr) delete[] pr;
    
//---------------------------------------------------------------------------
void gfx_main::resize(int _xs,int _ys)
    
    // release buffers
    if (scr) delete[] scr;
    if (zed)
        
        if (zed[0]) delete[] zed[0];
        delete[] zed;
        
    if (pl) delete[] pl;
    if (pr) delete[] pr;
    // set new resolution and pixelformat
    if ((_xs>0)&&(_ys>0)) bmp->SetSize(_xs,_ys);
    xs=bmp->Width;
    ys=bmp->Height;
    bmp->HandleType=bmDIB;
    bmp->PixelFormat=pf32bit;
    // allocate buffers
    scr=new int*[ys];
    zed=new int*[ys];
    zed[0]=new int[xs*ys];              // allocate depth buffer as single block
    for (int y=0;y<ys;y++)
        
        scr[y]=(int*)bmp->ScanLine[y];  // screen buffer point directly to VCL bitmap (back buffer)
        zed[y]=zed[0]+(y*xs);           // just set pointers for each depth line instead of allocating it
        
    pl=new pbuf[ys];
    pr=new pbuf[ys];
    
//---------------------------------------------------------------------------
int rgb2bgr(int col)                    // just support function reversing RGB order as VCL/GDI and its direct pixel access are not the same pixelformat
    
    union
        
        BYTE db[4];
        int  dd;
         c;
    BYTE q;
    c.dd=col;
    q=c.db[0]; c.db[0]=c.db[2]; c.db[2]=q;
    return c.dd;
    
//---------------------------------------------------------------------------
void gfx_main::clear(int z,int col)
    
    // clear buffers
    int x,y;
    col=rgb2bgr(col);
    for (y=0;y<ys;y++)
     for (x=0;x<xs;x++)
        
        scr[y][x]= 0x00000000; // black
        zed[y][x]=-0x7FFFFFFF; // as far as posible
        
    
//---------------------------------------------------------------------------
void gfx_main::pixel(int x,int y,int z,int col)
    
    col=rgb2bgr(col);
    if ((x>=0)&&(x<xs)&&(y>=0)&&(y<ys))         // inside screen
     if (zed[y][x]<=z)                          // not after something already rendered (GL_LEQUAL)
        
                                  zed[y][x]=z;  // update depth
        if (col!=col_transparent) scr[y][x]=col;// update color
        
    
//---------------------------------------------------------------------------
void gfx_main::line(int x0,int y0,int z0,int x1,int y1,int z1,int col)
    
    int i,n,x,y,z,kx,ky,kz,dx,dy,dz,cx,cy,cz;
    // DDA variables (d)abs delta,(k)step direction
    kx=0; dx=x1-x0; if (dx>0) kx=+1;  if (dx<0)  kx=-1; dx=-dx; 
    ky=0; dy=y1-y0; if (dy>0) ky=+1;  if (dy<0)  ky=-1; dy=-dy; 
    kz=0; dz=z1-z0; if (dz>0) kz=+1;  if (dz<0)  kz=-1; dz=-dz; 
    n=dx; if (n<dy) n=dy; if (n<dz) n=dz; if (!n) n=1;
    // integer DDA
    for (x=x0,y=y0,z=z0,cx=cy=cz=n,i=0;i<n;i++)
        
        pixel(x,y,z,col);
        cx-=dx; if (cx<=0) cx+=n; x+=kx; 
        cy-=dy; if (cy<=0) cy+=n; y+=ky; 
        cz-=dz; if (cz<=0) cz+=n; z+=kz; 
        
    
//---------------------------------------------------------------------------
void gfx_main::triangle(int x0,int y0,int z0,int x1,int y1,int z1,int x2,int y2,int z2,int col)
    
    int x,xx0,xx1,y,yy0,yy1,z,zz0,zz1,dz,dx,kz,cz;
    // boundary line coordinates to buffers
    _triangle_line(x0,y0,z0,x1,y1,z1);
    _triangle_line(x1,y1,z1,x2,y2,z2);
    _triangle_line(x2,y2,z2,x0,y0,z0);
    // y range
    yy0=y0; if (yy0>y1) yy0=y1; if (yy0>y2) yy0=y2;
    yy1=y0; if (yy1<y1) yy1=y1; if (yy1<y2) yy1=y2;
    // fill with horizontal lines
    for (y=yy0;y<=yy1;y++)
     if ((y>=0)&&(y<ys))
        
        if (pl[y].x<pr[y].x) xx0=pl[y].x; zz0=pl[y].z; xx1=pr[y].x; zz1=pr[y].z; 
        else                 xx1=pl[y].x; zz1=pl[y].z; xx0=pr[y].x; zz0=pr[y].z; 
              dx=xx1-xx0;
        kz=0; dz=zz1-zz0; if (dz>0) kz=+1;  if (dz<0)  kz=-1; dz=-dz; 
        for (cz=dx,x=xx0,z=zz0;x<=xx1;x++)
            
            pixel(x,y,z,col);
            cz-=dz; if (cz<=0) cz+=dx; z+=kz; 
            
        
    
//---------------------------------------------------------------------------
void gfx_main::_triangle_line(int x0,int y0,int z0,int x1,int y1,int z1)
    
    pbuf *pp;
    int i,n,x,y,z,kx,ky,kz,dx,dy,dz,cx,cy,cz;
    // DDA variables (d)abs delta,(k)step direction
    kx=0; dx=x1-x0; if (dx>0) kx=+1;  if (dx<0)  kx=-1; dx=-dx; 
    ky=0; dy=y1-y0; if (dy>0) ky=+1;  if (dy<0)  ky=-1; dy=-dy; 
    kz=0; dz=z1-z0; if (dz>0) kz=+1;  if (dz<0)  kz=-1; dz=-dz; 
    n=dx; if (n<dy) n=dy; if (n<dz) n=dz; if (!n) n=1;
    // target buffer according to ky direction
    if (ky>0) pp=pl; else pp=pr;
    // integer DDA line start point
    x=x0; y=y0;
    // fix endpoints just to be sure (wrong division constants by +/-1 can cause that last point is missing)
    if ((y0>=0)&&(y0<ys)) pp[y0].x=x0; pp[y0].z=z0; 
    if ((y1>=0)&&(y1<ys)) pp[y1].x=x1; pp[y1].z=z1; 
    // integer DDA (into pbuf)
    for (x=x0,y=y0,z=z0,cx=cy=cz=n,i=0;i<n;i++)
        
        if ((y>=0)&&(y<ys))
            
            pp[y].x=x;
            pp[y].z=z;
            
        cx-=dx; if (cx<=0) cx+=n; x+=kx; 
        cy-=dy; if (cy<=0) cy+=n; y+=ky; 
        cz-=dz; if (cz<=0) cz+=n; z+=kz; 
        
    
//---------------------------------------------------------------------------

只需忽略/移植 VCL 的东西。我刚刚将z 坐标添加到插值和渲染以及深度缓冲区。渲染代码如下所示:

//---------------------------------------------------------------------------
gfx_main gfx;
//---------------------------------------------------------------------------
float myFunc(float x,float y)
    
    float z;
    x-=gfx.xs/2;
    y-=gfx.ys/2;
    z=sqrt(((x*x)+(y*y))/((gfx.xs*gfx.xs)+(gfx.ys*gfx.ys)));    // normalized distance from center
    z=((0.25*cos(z*8.0*M_PI)*(1.0-z))+0.5)*gfx.ys;
    return z;
    
//---------------------------------------------------------------------------
void view3d(int &x,int &y,int &z)   // 3D -> 2D view (projection)
    
    int zz=z;
    z=y;
    x=x +(y/2)-(gfx.xs>>2);
    y=zz+(y/2)-(gfx.ys>>2);
    
//---------------------------------------------------------------------------
void draw()
    
    int i,x,y,z,ds,x0,y0,z0,x1,y1,z1,x2,y2,z2,x3,y3,z3,col;

    gfx.clear(-0x7FFFFFFF,0x00000000);

    // render
    ds=gfx.xs/50;
    for (i=0;i<2;i++)   // 2 passes
     for (y=ds;y<gfx.ys;y+=ds)
      for (x=ds;x<gfx.xs;x+=ds)
        
        // 4 vertexes of a quad face
        x0=x-ds; y0=y-ds; z0=myFunc(x0,y0);
        x1=x;    y1=y0;   z1=myFunc(x1,y1);
        x2=x;    y2=y;    z2=myFunc(x2,y2);
        x3=x0;   y3=y;    z3=myFunc(x3,y3);
        // camera transform
        view3d(x0,y0,z0);
        view3d(x1,y1,z1);
        view3d(x2,y2,z2);
        view3d(x3,y3,z3);
        if (i==0) // first pass
            
            // render (just to depth)
            col=col_transparent;
            gfx.triangle(x0,y0,z0,x1,y1,z1,x2,y2,z2,col);
            gfx.triangle(x0,y0,z0,x2,y2,z2,x3,y3,z3,col);
            
        if (i==1) // second pass
            
            // render wireframe
            col=0x00FF0000;
            gfx.line(x0,y0,z0,x1,y1,z1,col);
            gfx.line(x1,y1,z1,x2,y2,z2,col);
            gfx.line(x2,y2,z2,x3,y3,z3,col);
            gfx.line(x3,y3,z3,x0,y0,z0,col);
            
        
// here gfx.scr holds your rendered image
//---------------------------------------------------------------------------

不要忘记在渲染之前使用您的视图分辨率调用gfx.resize(xs,ys)。如您所见,我在这里使用了不同的功能(没关系):

这里和pixel(x,y,z,col)中没有深度条件的情况相同

pbuf 结构包含将在水平线的最后渲染插值中插值的所有内容。因此,如果您想要 gourard、纹理或其他任何内容,您只需将变量添加到此结构中并将插值添加到代码中(模仿 pbuf[].z 插值代码)

但是,这种方法有一个缺点。您当前的方法是逐个像素地插入一个轴,另一种是按网格大小步进。这个是按网格大小步进两个轴。因此,如果您想获得相同的行为,您可以使用1 x 1 quads 而不是ds x ds 进行第一遍,然后像现在一样执行这些行。如果您视图中的 1 对应于像素,那么您可以单独在像素上执行此操作而无需进行面部渲染,但是您可能会在输出中出现漏洞。

【讨论】:

以上是关于如何隐藏 3d 绘图的不可见元素?的主要内容,如果未能解决你的问题,请参考以下文章

如何让“.innerText”忽略不可见元素的不可见子元素?

jQuery 效果函数

jQuery 效果方法

jQuery 常用的效果函数

jQuery效果

Jquery效果函数