Canvas 的透视图
Perspective Vision on Canvas
今天我带来的话题是伪3D和透视
我正在查看视频 #1 Java Classical 3D Rendering Tutorial : Creating 3D World,他在视频中使用了一种渲染伪 3D 天花板和地板的方法。我试图找到一些教程或他使用的方法的名称,但我没有找到。我看到了算法,但不清楚理解。我开始搜索透视图形(消失点,horizon...),但我得到的独特之处是静态绘图。我想应用幻觉移动将相机放入计划中并移动它。下面按照我想做的透视地板和天花板的例子。
这只是一张图片,但我的第一个问题是:"I realy can make a movement of the camera in this ambient, like rotation and move x, and y axis?"。我试图在 canvas 中创建 2 个消失点,为每个 15º 的度数创建线条,我得到了透视错觉,但我找不到进行旋转或移动的方法。在那个视频中,我看到像素仅使用绿色和蓝色创建 2 个维度,但我想使用线条来制作它,以了解其工作原理。
没有一个地方是一步一步教如何用动作做透视的。我没有找到。我用视频的方法查了Java中3D游戏制作者的视频和"Prelude of the chambered"调用的Markus Person创建游戏的视频,但是没有找到这个渲染之王的解释。
假设我必须使用网格创建计划。我必须在线条中应用的逻辑如何创建运动?我真的很想了解制作这种伪 3D 的逻辑,而不使用框架或类似的东西。谢谢你帮我!我会等你的答复。
我查了一下SNES的MODE 7。我认为这是实现它的好方法。我只需要了解它是如何工作的,以及如何进行旋转。
** 注意:我不知道用光线投射来做什么。我将使用光线投射来创建墙壁。
有趣的问题。我没有抗拒并为了好玩而编写代码,所以这里有一些见解……嗯,有两种基本方法。一种是伪造的光栅,第二种是基于矢量的。我将描述后者,因为您可以用它做更多事情。
矢量方法
这种方法并没有伪造它真正的东西 3D。其余的取决于您要将其用于...的渲染...现在我假设您可以渲染 2D 行。所有代码块都在 C++.
中
变换
您需要矢量数学来转换世界和相机之间的点 space 并再次返回。在 3D 中,图形通常是 4x4 齐次变换矩阵 用于此和许多编程 API让我们原生支持他们。我的数学计算基于 OpenGL 矩阵布局,它决定了所使用的乘法顺序。有关更多信息,我强烈建议阅读此内容:
因为我经常用它。那里的链接答案也很有用,尤其是 3D 图形管道和全伪逆矩阵。简而言之,答案本身就是 3D 渲染所需的基本知识(低级别,除了渲染内容之外不需要任何库)。
还有像 GLM 这样的库,所以如果你愿意,你可以使用任何支持 4x4 矩阵和 4D 向量而不是我的代码。
所以让我们有两个 4x4
矩阵,一个 (camera
) 代表我们的相机坐标系,第二个 (icamera
) 是它的逆矩阵。现在如果我们想在世界和屏幕之间转换 space 我们只需这样做:
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线
我们可以使用任何图元进行渲染。我选择线是因为它非常简单并且可以实现很多。所以我们想要的是渲染3D线使用2D线渲染API(的任何一种)。我是基于 VCL 所以我选择了 VCL/GDI Canvas
这应该与你的 Canvas
.
非常相似
因此作为输入,我们在全球坐标系中得到了两个 3D 点。为了用 2D 行渲染它,我们需要将 3D 位置转换为 2D 屏幕 space。这是通过 matrix*vector
乘法完成的。
从中我们获得两个 3D 点,但在相机坐标系中。现在我们需要根据我们的视图区域 (Frustrum) 剪裁线。我们可以忽略 x,y
轴,因为 2D 行 api 通常会为我们这样做。所以唯一剩下的就是剪辑 z
轴。 z
轴中的 Frustrum 由 znear
和 zfar
定义。其中 zfar
是我们距相机焦点的最大可见距离。因此,如果我们的行完全位于 z-range
之前或之后,我们将忽略它并且不进行渲染。如果它在里面,我们渲染它。如果它穿过 znear
或 zfar
,我们会切掉外面的部分(通过 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
轴对齐线覆盖一些原点 O
周围的一些 size
的正方形.这些线之间的距离应该 step
等于网格单元格大小。
原点位置O
应该在我们的frustrun中心附近。如果它是恒定的,那么我们可以走出平面边缘,这样它就不会覆盖整个(半)屏幕。我们可以使用我们的相机位置并向其添加 0.5*(zfar+znear)*camera_z_axis
。为了保持运动的错觉,我们需要将 O
对齐到 step
大小。我们可以为此利用 floor
、round
或整数转换。
生成的平面代码如下所示:
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
只是我的 backbuffer 位图,以避免闪烁。键盘事件只是为了启用转动和移动(使用小键盘8,9,4,1
)。
此处预览以上代码:
现在,如果您想添加由 Fog 或 Volumetric fog 完成的 whiteout 能见度限制器。您只需根据参数 t
:
在渲染颜色和白色之间进行插值
t = (z-znear)/(zfar-znear); // t = <0,1>
其中 z
是相机中的像素坐标 space 所以:
color = color*(1.0-t) + White*t;
但是要在此处应用它,我们需要对 2D 行光栅化器进行编码,或者使用 2D 行 api每个顶点颜色(如 OpenGL)。另一种选择是通过混合雾图像来伪造它,该图像在中心线附近完全实心,在顶部和底部边缘完全透明。
我找到了老游戏中创建透视图的方法。在这里查看我的教程:http://programandocoisas.blogspot.com.br/2017/09/mode-7.html。
该方法的名称是MODE 7。我制作了一个教程来帮助想要实现和理解它的人。在纹理上制作模式 7 的公式是:
_X = X / Z
_Y = Y / Z
Z 可以用来创建深度。该变量只是 Y 坐标上的递增变量。获得_X和_Y新坐标后,只需使用这些坐标获得纹理中将要映射的像素,并将该像素插入渲染视图中的X Y坐标。
这是伪代码:
基本上伪代码是这样的:
//This is the pseudo-code to generate the basic mode7
for each y in the view do
y' <- y / z
for each x in the view do
x' <- x / z
put x',y' texture pixel value in x,y view pixel
end for
z <- z + 1
end for
代码如下:
package mode7;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
/**
* Mode 7 - Basic Implementation
* This code will map a texture to create a pseudo-3d perspective.
* This is an infinite render mode. The texture will be repeated without bounds.
* @author VINICIUS
*/
public class BasicModeSeven {
//Sizes
public static final int WIDTH = 800;
public static final int WIDTH_CENTER = WIDTH/2;
public static final int HEIGHT = 600;
public static final int HEIGHT_CENTER = HEIGHT/2;
/**
* @param args the command line arguments
*/
public static void main(String[] args) throws IOException {
//Create Frame
JFrame frame = new JFrame("Mode 7");
frame.setSize(WIDTH, HEIGHT);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
//Create Buffered Images:
//image - This is the image that will be printed in the render view
//texture - This is the image that will be mapped to the render view
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
BufferedImage texture = ImageIO.read(new File("src/mode7/texture.png"));
//The new coords that will be used to get the pixel on the texture
double _x, _y;
//z - the incrementable variable that beggins at -300 and go to 300, because
//the depth will be in the center of the HEIGHT
double z = HEIGHT_CENTER * -1;
//Scales just to control de scale of the printed pixel. It is not necessary
double scaleX = 16.0;
double scaleY = 16.0;
//Mode 7 - loop (Left Top to Down)
for(int y = 0; y < HEIGHT; y++){
_y = y / z; //The new _y coord generated
if(_y < 0)_y *= -1; //Control the _y because the z starting with a negative number
_y *= scaleY; //Increase the size using scale
_y %= texture.getHeight(); //Repeat the pixel avoiding get texture out of bounds
for(int x = 0; x < WIDTH; x++){
_x = (WIDTH_CENTER - x) / z; //The new _x coord generated
if(_x < 0)_x *= -1; //Control the _x to dont be negative
_x *= scaleX; //Increase the size using scale
_x %= texture.getWidth(); //Repeat the pixel avoiding get texture out of bounds
//Set x,y of the view image with the _x,_y pixel in the texture
image.setRGB(x, y, texture.getRGB((int)_x, (int)_y));
}
//Increment depth
z++;
}
//Loop to render the generated image
while(true){
frame.getGraphics().drawImage(image, 0, 0, null);
}
}
}
这是结果:
今天我带来的话题是伪3D和透视
我正在查看视频 #1 Java Classical 3D Rendering Tutorial : Creating 3D World,他在视频中使用了一种渲染伪 3D 天花板和地板的方法。我试图找到一些教程或他使用的方法的名称,但我没有找到。我看到了算法,但不清楚理解。我开始搜索透视图形(消失点,horizon...),但我得到的独特之处是静态绘图。我想应用幻觉移动将相机放入计划中并移动它。下面按照我想做的透视地板和天花板的例子。
这只是一张图片,但我的第一个问题是:"I realy can make a movement of the camera in this ambient, like rotation and move x, and y axis?"。我试图在 canvas 中创建 2 个消失点,为每个 15º 的度数创建线条,我得到了透视错觉,但我找不到进行旋转或移动的方法。在那个视频中,我看到像素仅使用绿色和蓝色创建 2 个维度,但我想使用线条来制作它,以了解其工作原理。
没有一个地方是一步一步教如何用动作做透视的。我没有找到。我用视频的方法查了Java中3D游戏制作者的视频和"Prelude of the chambered"调用的Markus Person创建游戏的视频,但是没有找到这个渲染之王的解释。
假设我必须使用网格创建计划。我必须在线条中应用的逻辑如何创建运动?我真的很想了解制作这种伪 3D 的逻辑,而不使用框架或类似的东西。谢谢你帮我!我会等你的答复。
我查了一下SNES的MODE 7。我认为这是实现它的好方法。我只需要了解它是如何工作的,以及如何进行旋转。
** 注意:我不知道用光线投射来做什么。我将使用光线投射来创建墙壁。
有趣的问题。我没有抗拒并为了好玩而编写代码,所以这里有一些见解……嗯,有两种基本方法。一种是伪造的光栅,第二种是基于矢量的。我将描述后者,因为您可以用它做更多事情。
矢量方法
这种方法并没有伪造它真正的东西 3D。其余的取决于您要将其用于...的渲染...现在我假设您可以渲染 2D 行。所有代码块都在 C++.
中变换
您需要矢量数学来转换世界和相机之间的点 space 并再次返回。在 3D 中,图形通常是 4x4 齐次变换矩阵 用于此和许多编程 API让我们原生支持他们。我的数学计算基于 OpenGL 矩阵布局,它决定了所使用的乘法顺序。有关更多信息,我强烈建议阅读此内容:
因为我经常用它。那里的链接答案也很有用,尤其是 3D 图形管道和全伪逆矩阵。简而言之,答案本身就是 3D 渲染所需的基本知识(低级别,除了渲染内容之外不需要任何库)。
还有像 GLM 这样的库,所以如果你愿意,你可以使用任何支持 4x4 矩阵和 4D 向量而不是我的代码。
所以让我们有两个
4x4
矩阵,一个 (camera
) 代表我们的相机坐标系,第二个 (icamera
) 是它的逆矩阵。现在如果我们想在世界和屏幕之间转换 space 我们只需这样做: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线
我们可以使用任何图元进行渲染。我选择线是因为它非常简单并且可以实现很多。所以我们想要的是渲染3D线使用2D线渲染API(的任何一种)。我是基于 VCL 所以我选择了 VCL/GDI
非常相似Canvas
这应该与你的Canvas
.因此作为输入,我们在全球坐标系中得到了两个 3D 点。为了用 2D 行渲染它,我们需要将 3D 位置转换为 2D 屏幕 space。这是通过
matrix*vector
乘法完成的。从中我们获得两个 3D 点,但在相机坐标系中。现在我们需要根据我们的视图区域 (Frustrum) 剪裁线。我们可以忽略
x,y
轴,因为 2D 行 api 通常会为我们这样做。所以唯一剩下的就是剪辑z
轴。z
轴中的 Frustrum 由znear
和zfar
定义。其中zfar
是我们距相机焦点的最大可见距离。因此,如果我们的行完全位于z-range
之前或之后,我们将忽略它并且不进行渲染。如果它在里面,我们渲染它。如果它穿过znear
或zfar
,我们会切掉外面的部分(通过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
轴对齐线覆盖一些原点O
周围的一些size
的正方形.这些线之间的距离应该step
等于网格单元格大小。原点位置
O
应该在我们的frustrun中心附近。如果它是恒定的,那么我们可以走出平面边缘,这样它就不会覆盖整个(半)屏幕。我们可以使用我们的相机位置并向其添加0.5*(zfar+znear)*camera_z_axis
。为了保持运动的错觉,我们需要将O
对齐到step
大小。我们可以为此利用floor
、round
或整数转换。生成的平面代码如下所示:
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
只是我的 backbuffer 位图,以避免闪烁。键盘事件只是为了启用转动和移动(使用小键盘8,9,4,1
)。
此处预览以上代码:
现在,如果您想添加由 Fog 或 Volumetric fog 完成的 whiteout 能见度限制器。您只需根据参数 t
:
t = (z-znear)/(zfar-znear); // t = <0,1>
其中 z
是相机中的像素坐标 space 所以:
color = color*(1.0-t) + White*t;
但是要在此处应用它,我们需要对 2D 行光栅化器进行编码,或者使用 2D 行 api每个顶点颜色(如 OpenGL)。另一种选择是通过混合雾图像来伪造它,该图像在中心线附近完全实心,在顶部和底部边缘完全透明。
我找到了老游戏中创建透视图的方法。在这里查看我的教程:http://programandocoisas.blogspot.com.br/2017/09/mode-7.html。 该方法的名称是MODE 7。我制作了一个教程来帮助想要实现和理解它的人。在纹理上制作模式 7 的公式是:
_X = X / Z
_Y = Y / Z
Z 可以用来创建深度。该变量只是 Y 坐标上的递增变量。获得_X和_Y新坐标后,只需使用这些坐标获得纹理中将要映射的像素,并将该像素插入渲染视图中的X Y坐标。
这是伪代码: 基本上伪代码是这样的:
//This is the pseudo-code to generate the basic mode7
for each y in the view do
y' <- y / z
for each x in the view do
x' <- x / z
put x',y' texture pixel value in x,y view pixel
end for
z <- z + 1
end for
代码如下:
package mode7;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
/**
* Mode 7 - Basic Implementation
* This code will map a texture to create a pseudo-3d perspective.
* This is an infinite render mode. The texture will be repeated without bounds.
* @author VINICIUS
*/
public class BasicModeSeven {
//Sizes
public static final int WIDTH = 800;
public static final int WIDTH_CENTER = WIDTH/2;
public static final int HEIGHT = 600;
public static final int HEIGHT_CENTER = HEIGHT/2;
/**
* @param args the command line arguments
*/
public static void main(String[] args) throws IOException {
//Create Frame
JFrame frame = new JFrame("Mode 7");
frame.setSize(WIDTH, HEIGHT);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
//Create Buffered Images:
//image - This is the image that will be printed in the render view
//texture - This is the image that will be mapped to the render view
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
BufferedImage texture = ImageIO.read(new File("src/mode7/texture.png"));
//The new coords that will be used to get the pixel on the texture
double _x, _y;
//z - the incrementable variable that beggins at -300 and go to 300, because
//the depth will be in the center of the HEIGHT
double z = HEIGHT_CENTER * -1;
//Scales just to control de scale of the printed pixel. It is not necessary
double scaleX = 16.0;
double scaleY = 16.0;
//Mode 7 - loop (Left Top to Down)
for(int y = 0; y < HEIGHT; y++){
_y = y / z; //The new _y coord generated
if(_y < 0)_y *= -1; //Control the _y because the z starting with a negative number
_y *= scaleY; //Increase the size using scale
_y %= texture.getHeight(); //Repeat the pixel avoiding get texture out of bounds
for(int x = 0; x < WIDTH; x++){
_x = (WIDTH_CENTER - x) / z; //The new _x coord generated
if(_x < 0)_x *= -1; //Control the _x to dont be negative
_x *= scaleX; //Increase the size using scale
_x %= texture.getWidth(); //Repeat the pixel avoiding get texture out of bounds
//Set x,y of the view image with the _x,_y pixel in the texture
image.setRGB(x, y, texture.getRGB((int)_x, (int)_y));
}
//Increment depth
z++;
}
//Loop to render the generated image
while(true){
frame.getGraphics().drawImage(image, 0, 0, null);
}
}
}
这是结果: