今天,我将介绍有关伪3D和透视的主题。

我正在检查视频#1 Java Classical 3D Rendering Tutorial : Creating 3D World,他在其中使用了一种渲染伪3D天花板和地板的方法。我试图找到一些教程或他使用的方法的名称,但没有找到。我看到了算法,但不清楚。我开始搜索透视图(消失点,地平线...),但是我得到的独特之处是静态绘图。我想施加一种幻觉,那就是将相机放入计划中并进行移动。在下面的示例中,我要制作透视地板和天花板。

java -  Canvas 上的透视图-LMLPHP
java -  Canvas 上的透视图-LMLPHP

这只是一张图像,但是我的第一个问题是:“我真的可以在这种环境下使相机运动,例如旋转并移动x和y轴吗?”。我试图在画布上画出2个消失点,为每度15º创建线条,但我有一个透视幻觉,但我找不到旋转或移动的方法。在该视频中,我看到了像素仅使用绿色和蓝色创建了2个尺寸,但是我想使用线条进行设置,以了解其工作原理。

java -  Canvas 上的透视图-LMLPHP

没有一个地方可以逐步教授如何用动作制作视角。我没找到我使用视频的方法检查了Java 3D游戏制造商的视频以及Markus Person创建的游戏,该游戏被称为“密室前奏”,但我没有找到有关渲染之王的解释。

java -  Canvas 上的透视图-LMLPHP

让我们假设我必须使用网格创建一个计划。我在生产运动中必须遵循的逻辑是什么?我真的想了解在不使用框架之类的情况下制作这种伪3D的逻辑。谢谢帮我!我会等你的回答。

我检查了有关SNES的MODE 7的一些信息。我认为这是实现这一目标的好方法。我只需要了解它是如何工作的,以及如何进行轮换。

java -  Canvas 上的透视图-LMLPHP

**注意:我不使用射线广播。我将使用光线投射来创建墙。

最佳答案

有趣的问题。我并不是为了娱乐而对其进行抵抗和编码,因此这里有一些见解...好吧,这里有2种基本方法。一个是栅格伪造的,第二个是基于向量的。我将描述后者,因为您可以使用它做更多的事情。

向量法

这种方法不会伪造真正的3D效果。其余的取决于您要用于此的渲染...现在,我假设您可以渲染2D线。所有代码块都在C ++中。


转变

您需要向量数学才能在世界和相机空间之间转换点,然后再转换回来。在3D图形中,通常为此使用4x4均匀变换矩阵,并且许多编程API本身都支持它们。我将基于OpenGL矩阵布局确定数学,该矩阵确定使用的乘法顺序。有关更多信息,我强烈建议阅读以下内容:


Understanding 4x4 homogenous transform matrices


因为我从中使用了很多东西。那里的链接答案尤其有用,尤其是3D图形管线和Full伪逆矩阵。简而言之,答案本身就是3D渲染所需的基础知识(底层,无需任何渲染即可)。

也有类似GLM的库,因此如果您愿意,可以使用支持4x4矩阵和4D向量的任何线性代数代替我的代码。

因此,让我们有两个4x4矩阵,其中一个(camera)代表我们的相机坐标系,第二个(icamera)是它的逆矩阵。现在,如果我们要在世界和屏幕空间之间转换,只需执行以下操作:

P = camera*Q
Q = icamera*P


其中P(x,y,z,1)是相机坐标系中的点,而Q(x,y,z,1)是全局世界坐标系中的同一点。
透视

只需将P除以其z坐标即可完成。这将围绕(0,0)缩放对象,因此对象越远,则对象越小。如果我们添加一些屏幕分辨率和轴校正,则可以使用以下方法:

void perspective(double *P) // apply perspective transform on P
    {
    // perspectve division
    P[0]*=znear/P[2];
    P[1]*=znear/P[2];
    // screen coordinate system
    P[0]=xs2+P[0];          // move (0,0) to screen center
    P[1]=ys2-P[1];          // axises: x=right, y=up
    }


所以点0,0是屏幕的中心。 xs2,ys2是屏幕分辨率的一半,而znear是投影的焦距。因此,具有屏幕分辨率且以XY为中心的(0,0,znear)平面矩形将完全覆盖屏幕。
渲染3D线

我们可以使用任何原语进行渲染。我选择行是因为它非常简单并且可以实现很多目标。因此,我们要使用2D线渲染API(任何类型)来渲染3D线。我基于VCL,所以我选择了VCL/GDI Canvas,它应该与您的Canvas非常相似。

因此,作为输入,我们在全球世界坐标系中获得了两个3D点。为了用2D线渲染它,我们需要将3D位置转换为2D屏幕空间。这是通过matrix*vector乘法完成的。

由此,我们获得了两个3D点,但它们位于相机坐标系中。现在,我们需要按视图区域(Frustrum)修剪线。我们可以忽略x,y轴,因为2D线api通常无论如何都会为我们做到这一点。因此,剩下的唯一内容就是clip z轴。 z轴中的视锥由znearzfar定义。其中zfar是我们到相机焦点的最大可见距离。因此,如果我们的行完全在我们的z-range之前或之后,我们将忽略它并且不渲染。如果它在里面我们渲染它。如果它越过znearzfar,我们将外部切掉(通过x,y坐标的线性插值)。

现在,我们仅在两个点上应用透视图,并使用它们的x,y坐标渲染2D线。

我的代码如下所示:

void draw_line(TCanvas *can,double *pA,double *pB)  // draw 3D line
    {
    int i;
    double D[3],A[3],B[3],t;
    // transform to camera coordinate system
    matrix_mul_vector(A,icamera,pA);
    matrix_mul_vector(B,icamera,pB);
    // sort points so A.z<B.z
    if (A[2]>B[2]) for (i=0;i<3;i++) { D[i]=A[i]; A[i]=B[i]; B[i]=D[i]; }
    // D = B-A
    for (i=0;i<3;i++) D[i]=B[i]-A[i];
    // ignore out of Z view lines
    if (A[2]>zfar) return;
    if (B[2]<znear) return;
    // cut line to view if needed
    if (A[2]<znear)
        {
        t=(znear-A[2])/D[2];
        A[0]+=D[0]*t;
        A[1]+=D[1]*t;
        A[2]=znear;
        }
    if (B[2]>zfar)
        {
        t=(zfar-B[2])/D[2];
        B[0]+=D[0]*t;
        B[1]+=D[1]*t;
        B[2]=zfar;
        }
    // apply perspective
    perspective(A);
    perspective(B);
    // render
    can->MoveTo(A[0],A[1]);
    can->LineTo(B[0],B[1]);
    }

渲染XZ平面

我们可以使用3D线作为正方形网格来可视化地面和天空平面。因此,我们只需创建for循环,以绘制x轴对齐的线和y轴对齐的线,这些线围绕某个原点位置size覆盖某个O的某个正方形。线之间应相距step,等于网格像元大小。

原点位置O应该在我们的frustrun中心附近。如果它是恒定的,那么我们可以走出平面边缘,这样它就不会覆盖整个(半个)屏幕。我们可以使用相机的位置并在其中添加0.5*(zfar+znear)*camera_z_axis。为了保持运动的错觉,我们需要将O调整为step的大小。为此,我们可以利用floorround或整型强制转换。

生成的平面代码如下所示:

void draw_plane_xz(TCanvas *can,double y,double step) // draw 3D plane
    {
    int i;
    double A[3],B[3],t,size;
    double U[3]={1.0,0.0,0.0};  // U = X
    double V[3]={0.0,0.0,1.0};  // V = Z
    double O[3]={0.0,0.0,0.0};  // Origin
    // compute origin near view center but align to step
    i=0; O[i]=floor(camera[12+i]/step)*step;
    i=2; O[i]=floor(camera[12+i]/step)*step;
    O[1]=y;
    // set size so plane safely covers whole view
    t=xs2*zfar/znear;               size=t; // x that will convert to xs2 at zfar
    t=0.5*(zfar+znear); if (size<t) size=t; // half of depth range
    t+=step;                                // + one grid cell beacuse O is off up to 1 grid cell
    t*=sqrt(2);                             // diagonal so no matter how are we rotate in Yaw
    // U lines
    for (i=0;i<3;i++)
        {
        A[i]=O[i]+(size*U[i])-((step+size)*V[i]);
        B[i]=O[i]-(size*U[i])-((step+size)*V[i]);
        }
    for (t=-size;t<=size;t+=step)
        {
        for (i=0;i<3;i++)
            {
            A[i]+=step*V[i];
            B[i]+=step*V[i];
            }
        draw_line(can,A,B);
        }
    // V lines
    for (i=0;i<3;i++)
        {
        A[i]=O[i]-((step+size)*U[i])+(size*V[i]);
        B[i]=O[i]-((step+size)*U[i])-(size*V[i]);
        }
    for (t=-size;t<=size;t+=step)
        {
        for (i=0;i<3;i++)
            {
            A[i]+=step*U[i];
            B[i]+=step*U[i];
            }
        draw_line(can,A,B);
        }
    matrix_mul_vector(A,icamera,A);
    }



现在,如果将所有这些放在一起在小型VCL / GDI / Canvas应用程序中,我会得到:

//---------------------------------------------------------------------------
#include <vcl.h> // you can ignore these lines
#include <math.h>
#pragma hdrstop
#include "win_main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm" // up to here.
TMain *Main; // this is pointer to my VCL window (you do not need it)
//--- Here starts the important stuff: --------------------------------------
// perspective
double znear= 100.0;    // focal length for perspective
double zfar = 2100.0;   // visibility
// view
double xs2=0.0;         // screen half resolution
double ys2=0.0;
// camera
double yaw=0.0;         // euler yaw angle [rad]
double camera[16];      // camera direct transform matrix
double icamera[16];     // camera inverse transform matrix
// keyboard bools
bool _forw=false,_back=false,_right=false,_left=false;
//---------------------------------------------------------------------------
void matrix_inv(double *a,double *b) // a[16] = Inverse(b[16])
    {
    double x,y,z;
    // transpose of rotation matrix
    a[ 0]=b[ 0];
    a[ 5]=b[ 5];
    a[10]=b[10];
    x=b[1]; a[1]=b[4]; a[4]=x;
    x=b[2]; a[2]=b[8]; a[8]=x;
    x=b[6]; a[6]=b[9]; a[9]=x;
    // copy projection part
    a[ 3]=b[ 3];
    a[ 7]=b[ 7];
    a[11]=b[11];
    a[15]=b[15];
    // convert origin: new_pos = - new_rotation_matrix * old_pos
    x=(a[ 0]*b[12])+(a[ 4]*b[13])+(a[ 8]*b[14]);
    y=(a[ 1]*b[12])+(a[ 5]*b[13])+(a[ 9]*b[14]);
    z=(a[ 2]*b[12])+(a[ 6]*b[13])+(a[10]*b[14]);
    a[12]=-x;
    a[13]=-y;
    a[14]=-z;
    }
//---------------------------------------------------------------------------
void  matrix_mul_vector(double *c,double *a,double *b) // c[3] = a[16]*b[3]
    {
    double q[3];
    q[0]=(a[ 0]*b[0])+(a[ 4]*b[1])+(a[ 8]*b[2])+(a[12]);
    q[1]=(a[ 1]*b[0])+(a[ 5]*b[1])+(a[ 9]*b[2])+(a[13]);
    q[2]=(a[ 2]*b[0])+(a[ 6]*b[1])+(a[10]*b[2])+(a[14]);
    for(int i=0;i<3;i++) c[i]=q[i];
    }
//---------------------------------------------------------------------------
void compute_matrices() // recompute camera,icamera after camera position or yaw change
    {
    // bound angle
    while (yaw>2.0*M_PI) yaw-=2.0*M_PI;
    while (yaw<0.0     ) yaw+=2.0*M_PI;
    // X = right
    camera[ 0]= cos(yaw);
    camera[ 1]=     0.0 ;
    camera[ 2]= sin(yaw);
    // Y = up
    camera[ 4]=     0.0 ;
    camera[ 5]=     1.0 ;
    camera[ 6]=     0.0 ;
    // Z = forward
    camera[ 8]=-sin(yaw);
    camera[ 9]=     0.0 ;
    camera[10]= cos(yaw);
    // no projection
    camera[ 3]=     0.0 ;
    camera[ 7]=     0.0 ;
    camera[11]=     0.0 ;
    camera[15]=     1.0 ;
    // compute the inverse matrix
    matrix_inv(icamera,camera);
    }
//---------------------------------------------------------------------------
void perspective(double *P) // apply perspective transform
    {
    // perspectve division
    P[0]*=znear/P[2];
    P[1]*=znear/P[2];
    // screen coordinate system
    P[0]=xs2+P[0];          // move (0,0) to screen center
    P[1]=ys2-P[1];          // axises: x=right, y=up
    }
//---------------------------------------------------------------------------
void draw_line(TCanvas *can,double *pA,double *pB)  // draw 3D line
    {
    int i;
    double D[3],A[3],B[3],t;
    // transform to camera coordinate system
    matrix_mul_vector(A,icamera,pA);
    matrix_mul_vector(B,icamera,pB);
    // sort points so A.z<B.z
    if (A[2]>B[2]) for (i=0;i<3;i++) { D[i]=A[i]; A[i]=B[i]; B[i]=D[i]; }
    // D = B-A
    for (i=0;i<3;i++) D[i]=B[i]-A[i];
    // ignore out of Z view lines
    if (A[2]>zfar) return;
    if (B[2]<znear) return;
    // cut line to view if needed
    if (A[2]<znear)
        {
        t=(znear-A[2])/D[2];
        A[0]+=D[0]*t;
        A[1]+=D[1]*t;
        A[2]=znear;
        }
    if (B[2]>zfar)
        {
        t=(zfar-B[2])/D[2];
        B[0]+=D[0]*t;
        B[1]+=D[1]*t;
        B[2]=zfar;
        }
    // apply perspective
    perspective(A);
    perspective(B);
    // render
    can->MoveTo(A[0],A[1]);
    can->LineTo(B[0],B[1]);
    }
//---------------------------------------------------------------------------
void draw_plane_xz(TCanvas *can,double y,double step) // draw 3D plane
    {
    int i;
    double A[3],B[3],t,size;
    double U[3]={1.0,0.0,0.0};  // U = X
    double V[3]={0.0,0.0,1.0};  // V = Z
    double O[3]={0.0,0.0,0.0};  // Origin
    // compute origin near view center but align to step
    i=0; O[i]=floor(camera[12+i]/step)*step;
    i=2; O[i]=floor(camera[12+i]/step)*step;
    O[1]=y;
    // set size so plane safely covers whole view
    t=xs2*zfar/znear;               size=t; // x that will convert to xs2 at zfar
    t=0.5*(zfar+znear); if (size<t) size=t; // half of depth range
    t+=step;                                // + one grid cell beacuse O is off up to 1 grid cell
    t*=sqrt(2);                             // diagonal so no matter how are we rotate in Yaw
    // U lines
    for (i=0;i<3;i++)
        {
        A[i]=O[i]+(size*U[i])-((step+size)*V[i]);
        B[i]=O[i]-(size*U[i])-((step+size)*V[i]);
        }
    for (t=-size;t<=size;t+=step)
        {
        for (i=0;i<3;i++)
            {
            A[i]+=step*V[i];
            B[i]+=step*V[i];
            }
        draw_line(can,A,B);
        }
    // V lines
    for (i=0;i<3;i++)
        {
        A[i]=O[i]-((step+size)*U[i])+(size*V[i]);
        B[i]=O[i]-((step+size)*U[i])-(size*V[i]);
        }
    for (t=-size;t<=size;t+=step)
        {
        for (i=0;i<3;i++)
            {
            A[i]+=step*U[i];
            B[i]+=step*U[i];
            }
        draw_line(can,A,B);
        }
    matrix_mul_vector(A,icamera,A);
    }
//---------------------------------------------------------------------------
void TMain::draw() // this is my main rendering routine
    {
    // clear buffer
    bmp->Canvas->Brush->Color=clWhite;
    bmp->Canvas->FillRect(TRect(0,0,xs,ys));
    // init/update variables
    double step= 50.0;                              // plane grid size
    ::xs2=Main->xs2;                                // update actual screen half resolution
    ::ys2=Main->ys2;
    // sky
    bmp->Canvas->Pen->Color=clBlue;
    draw_plane_xz(bmp->Canvas,+200.0,step);
    // terrain
    bmp->Canvas->Pen->Color=clGreen;
    draw_plane_xz(bmp->Canvas,-200.0,step);
    // render backbuffer
    Main->Canvas->Draw(0,0,bmp);
    _redraw=false;
    }
//---------------------------------------------------------------------------
__fastcall TMain::TMain(TComponent* Owner) : TForm(Owner) // this is initialization
    {
    bmp=new Graphics::TBitmap;
    bmp->HandleType=bmDIB;
    bmp->PixelFormat=pf32bit;
    pyx=NULL;
    _redraw=true;


    // camera start position
    camera[12]=0.0;
    camera[13]=0.0;
    camera[14]=0.0;
    compute_matrices();
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormDestroy(TObject *Sender) // this is exit
    {
    if (pyx) delete[] pyx;
    delete bmp;
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormResize(TObject *Sender) // this is called on resize
    {
    xs=ClientWidth;  xs2=xs>>1;
    ys=ClientHeight; ys2=ys>>1;
    bmp->Width=xs;
    bmp->Height=ys;
    if (pyx) delete[] pyx;
    pyx=new int*[ys];
    for (int y=0;y<ys;y++) pyx[y]=(int*) bmp->ScanLine[y];
    _redraw=true;
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormPaint(TObject *Sender) // this is called on forced repaint
    {
    _redraw=true;
    }
//---------------------------------------------------------------------------
void __fastcall TMain::tim_redrawTimer(TObject *Sender) // this is called periodically by my timer
    {
    double da=5.0*M_PI/180.0;   // turn speed
    double dl=15.0;             // movement speed
    bool _recompute=false;
    if (_left ) { _redraw=true; _recompute=true; yaw+=da; }
    if (_right) { _redraw=true; _recompute=true; yaw-=da; }
    if (_forw ) { _redraw=true; _recompute=true; for (int i=0;i<3;i++) camera[12+i]+=dl*camera[8+i]; }
    if (_back ) { _redraw=true; _recompute=true; for (int i=0;i<3;i++) camera[12+i]-=dl*camera[8+i]; }
    if (_recompute) compute_matrices();
    if (_redraw) draw();
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormKeyDown(TObject *Sender, WORD &Key,TShiftState Shift) // this is called when key is pushed
    {
    //Caption=Key;
    if (Key==104) _left=true;
    if (Key==105) _right=true;
    if (Key==100) _forw=true;
    if (Key== 97) _back=true;
    }
//---------------------------------------------------------------------------
void __fastcall TMain::FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift) // this is called when key is released
    {
    if (Key==104) _left=false;
    if (Key==105) _right=false;
    if (Key==100) _forw=false;
    if (Key== 97) _back=false;
    }
//---------------------------------------------------------------------------


这是Form头文件(除非您在我的VCL应用程序中进行重构,否则您实际上并不需要它)

//---------------------------------------------------------------------------

#ifndef win_mainH
#define win_mainH
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ComCtrls.hpp>
#include <ExtCtrls.hpp>
//---------------------------------------------------------------------------
class TMain : public TForm
{
__published:    // IDE-managed Components
    TTimer *tim_redraw;
    void __fastcall FormResize(TObject *Sender);
    void __fastcall FormPaint(TObject *Sender);
    void __fastcall FormDestroy(TObject *Sender);
    void __fastcall tim_redrawTimer(TObject *Sender);
    void __fastcall FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift);
    void __fastcall FormKeyUp(TObject *Sender, WORD &Key, TShiftState Shift);
private:    // User declarations
public:     // User declarations
    __fastcall TMain(TComponent* Owner);
    void draw();

    int xs,ys,xs2,ys2,**pyx;
    Graphics::TBitmap *bmp;
    bool _redraw;
};
//---------------------------------------------------------------------------
extern PACKAGE TMain *Main;
//---------------------------------------------------------------------------
#endif


VCL应用程序只是具有单个计时器(100ms)的单一表单,而没有其他VCL组件。 bmp只是我的后缓冲位图,以避免闪烁。键盘事件仅用于启用旋转和移动(使用数字键盘8,9,4,1)。

这里预览上面的代码:

java -  Canvas 上的透视图-LMLPHP

现在,如果要添加由雾或体积雾完成的变白可见性限制器。您只需根据参数t在渲染的颜色和白色之间进行插值:

t = (z-znear)/(zfar-znear); // t = <0,1>


其中z是相机空间中的像素坐标,因此:

color = color*(1.0-t) + White*t;


但是要在此处应用,我们将需要对2D线光栅化器进行编码或使用具有每个顶点颜色的2D线api(例如OpenGL)。另一种选择是通过混合Fog图像来伪造它,该图像在中心线附近完全实心,并且在顶部和底部边缘完全透明。

08-06 03:53
查看更多