上一篇博客(C语言伪3D制作-CSDN博客)介绍了一种迷惑性的“伪3D”成像方式,这一次分享一种常用于第一人称游戏的摄像头成像方式和C语言3D贴图的制作。
文件下载地址:文件下载-奶牛快传 Download |CowTransfer
(程序需要使用EGE图形库,不支持easyX,如果用的是easyX,请自行更改代码,或下载自带EGE的redpandaDEVc++(EGE与easyX不兼容),下载地址:文件下载-奶牛快传 Download |CowTransfer,EGE官网:Easy Graphics Engine))
为便于使用,把要用的函数都打包成.h文件了
成品:
一、摄像头成像方式
这种成像方式简单来说就是模拟一个摄像头,把摄像头收集到的图像显示出来,我们可以创建一个镜头焦距可变的摄像头,镜头光心z轴为oz,像距为l,左右视角为va。
之后我们就可以根据va求出对应的像距l为getwidth()/2/tan(va*PI/360) (在上一次的“伪3D”里也适用),那么就可以定义函数defineva()为
void defineva(float a)
{
va=a;
l=getwidth()/2/tan(a*PI/360);
}
然后再在坐标上确定一个点t,但是因为摄像头成的是倒像,所以要把得到的像“倒”过来,那么t在屏幕上的坐标就为:(ox+t.x*l/(oz-t.z),oy+t.y*l/(oz-t.z))
(ox,oy为屏幕中心点x、y坐标)
那么我们就可以定义一个函数来绘制3d点,如下:
void draw3ddot(VECTOR3D *t,color_t c)
{
if(t->z>oz){
return;
}
//mode为储存成像方式的bool变量,0为“伪3D”,1为摄像头成像方式
if(mode==0){
putpixel(ox+t->x*l/(l+oz-t->z),oy+t->y*l/(l+oz-t->z),c);
}
else
{
putpixel(ox+t->x*l/(oz-t->z),oy+t->y*l/(oz-t->z),c);
//画点函数,根据计算出的位置和引用的color_t变量在屏幕上绘制点
}
}
二、3D贴图制作
1、原理
制作3D贴图的主要原理就是把平面上的点全部列举出来,然后计算出点在屏幕上的位置和对应的颜色并绘制在屏幕上。
2、计算方法
首先我们确定需要引用的元素,用VECTOR3D结构体lt,lb,rt,rb来表示图像的左上、左下、右上、右下坐标,再引用一个PIMAGE图片,把图片“贴”上去。
然后,创建int变量w和h(必须要用int,因为w和h代表的是分成的份数,尽量接近1像素),储存最大的横向边长和纵向边长,可以用3D两点距离公式 l=√(a.x-b.x)²+(a.y-b.y)²+(a.z-b.z)²求出,再取出平面中z轴最大的点,根据这个点的z轴对应的x,y平面大小与屏幕的大小之比给w、h进行缩放,减少漏算和不必要的计算。
这样我们就可以依次遍历点。依靠遍历点的x(列)和y(行),根据图形的相似求出点t所在的两条线段的端点坐标为:
a1.x=lt.x+(rt.x-lt.x)*i/w;
a1.y=lt.y+(rt.y-lt.y)*i/w;
a1.z=lt.z+(rt.z-lt.z)*i/w;
a2.x=lb.x+(rb.x-lb.x)*i/w;
a2.y=lb.y+(rb.y-lb.y)*i/w;
a2.z=lb.z+(rb.z-lb.z)*i/w;(i为遍历的x)
b1.x=lt.x+(lb.x-lt.x)*j/h;
b1.y=lt.y+(lb.y-lt.y)*j/h;
b1.z=lt.z+(lb.z-lt.z)*j/h;
b2.x=rt.x+(rb.x-rt.x)*j/h;
b2.y=rt.y+(rb.y-rt.y)*j/h;
b2.z=rt.z+(rb.z-rt.z)*j/h;(j为遍历的y)
由此,可以列一个方程组,设ta1:a1a2=a,tb1:b1b2=b,则:
(1) a1.x+a*(a2.x-a1.x)=b1.x+b*(b2.x-b1.x),
(2) a1.y+a*(a2.y-a1.y)=b1.y+b*(b2.y-b1.y),
(3) a1.z+a*(a2.z-a1.z)=b1.z+b*(b2.z-b1.z),
解方程(1)、(2),得:
a=((a1.x-b1.x)/(b2.x-b1.x)-(a1.y-b1.y)/(b2.y-b1.y))/((a2.y-a1.y)/(b2.y-b1.y)-(a2.x-a1.x)/(b2.x-b1.x));
但是当a2.x-a1.x=0或b2.x-b1.x=0时,方程的解就会出错,所以我们应该根据不同的情况列出不同的方程,如下:
当a2.x==a1.x && b2.x!=b1.x:
b=(a1.x-b1.x)/(b2.x-b1.x);
当b2.x==b1.x && a2.x!=a1.x:
a=(b1.x-a1.x)/(a2.x-a1.x);
当a1.y==a2.y && b1.y!=b2.y:
b=(a1.y-b1.y)/(b2.y-b1.y);
当b1.y==b2.y && a1.y!=a2.y:
a=(b1.y-a1.y)/(a2.y-a1.y);
当a1.z==a2.z && b1.z!=b2.z:
b=(a1.z-b1.z)/(b2.z-b1.z);
当b1.z==b2.z && a1.z!=a2.z:
a=(b1.x-a1.x)/(a2.x-a1.x);
当a1.x!=a2.x && b1.x!=b2.x && a1.y!=a2.y && b1.y!=b2.y:
a=((a1.x-b1.x)/(b2.x-b1.x)-(a1.y-b1.y)/(b2.y-b1.y))/((a2.y-a1.y)/(b2.y-b1.y)-(a2.x-a1.x)/(b2.x-b1.x));
当a1.y!=a2.y && b1.y!=b2.y && a1.z!=a2.z && b1.z!=b2.z:
a=((a1.y-b1.y)/(b2.y-b1.y)-(a1.z-b1.z)/(b2.z-b1.z))/((a2.z-a1.z)/(b2.z-b1.z)-(a2.y-a1.y)/(b2.y-b1.y));
当a1.x!=a2.x && b1.x!=b2.x && a1.z!=a2.z && b1.z!=b2.z:
a=((a1.x-b1.x)/(b2.x-b1.x)-(a1.z-b1.z)/(b2.z-b1.z))/((a2.z-a1.z)/(b2.z-b1.z)-(a2.x-a1.x)/(b2.x-b1.x));
解出a,b后就可以根据
t.x=a1.x+a*(a2.x-a1.x); t.x=b1.x+b*(b2.x-b1.x);
t.y=a1.y+a*(a2.y-a1.y); t.y=b1.y+b*(b2.y-b1.y);
t.z=a1.z+a*(a2.z-a1.z); t.z=b1.z+b*(b2.z-b1.z);
求出t的坐标,使用这种方法不止可以实现3D贴图,还可以实现图像的局部缩放。
求出t的坐标后,我们还可以根据遍历t点的x(列)、y(行)求出图像中对应的像素颜色。
首先,我们要先定义一个color_t*指针p=getbuffer(image);,用于查看和改变图像像素,查看方式为p[y*getwidth(image)+x],则遍历t点的x(列)、y(行)对应的图像像素就是
p[x*getheight(image)/h*getwidth(image)+y*getwidth(image)/w]
这样就可以改变图像的形状(凹四边形除外),如果不需要,可以直接利用等比例线段来求交点,但是这样只能把图像转换成长方形、梯形、平行四边形或三角形。
3、判断重合
加载图像的时候如果直接绘制图片就会失去空间感,没有层次,那么我们就应该在画点的时候加一个判断,判断是否被其他点挡住。
为了判断重合,我们应该创建一个数组,储存屏幕上所有像素对应点的z轴,数组长度就应该为屏幕高度*屏幕宽度,但是如果屏幕太大,就会超过C语言的最大数组长度,那么我们可以使用动态内存定义float*指针wxy = (float*)malloc(l*sizeof(float));,l为分配长度,如果要改变长度,可以使wxy=(float*)realloc(wxy,l*sizeof(float));,查询方式和一维数组一样(本来想用这个来创建队列,结果一运行程序就直接退出了)
4、代码
wxy的定义和“归零”(默认值为屏幕z轴):
float *wxy = (float*)malloc(0*sizeof(float));
void emptywxy(float *wxy)
{
int i;
for(i=0;i<getwidth()*getheight();i++){
*(wxy+i)=oz;
}
}
求3D直线交点函数(为了节省内存,所以都引用指针,反正也不改变变量的值):
VECTOR3D findIntersection(VECTOR3D *a1, VECTOR3D *a2, VECTOR3D *b1, VECTOR3D *b2) {
VECTOR3D t;
float p=0;
if(a2->x==a1->x && b2->x!=b1->x)
{
p=(a1->x-b1->x)/(b2->x-b1->x);
t.x=b1->x+p*(b2->x-b1->x);
t.y=b1->y+p*(b2->y-b1->y);
t.z=b1->z+p*(b2->z-b1->z);
return t;
}
if(b2->x==b1->x && a2->x!=a1->x)
{
p=(b1->x-a1->x)/(a2->x-a1->x);
t.x=a1->x+p*(a2->x-a1->x);
t.y=a1->y+p*(a2->y-a1->y);
t.z=a1->z+p*(a2->z-a1->z);
return t;
}
if(a1->y==a2->y && b1->y!=b2->y)
{
p=(a1->y-b1->y)/(b2->y-b1->y);
t.x=b1->x+p*(b2->x-b1->x);
t.y=b1->y+p*(b2->y-b1->y);
t.z=b1->z+p*(b2->z-b1->z);
return t;
}
if(b1->y==b2->y && a1->y!=a2->y)
{
p=(b1->y-a1->y)/(a2->y-a1->y);
t.x=a1->x+p*(a2->x-a1->x);
t.y=a1->y+p*(a2->y-a1->y);
t.z=a1->z+p*(a2->z-a1->z);
return t;
}
if(a1->z==a2->z && b1->z!=b2->z)
{
p=(a1->z-b1->z)/(b2->z-b1->z);
t.x=b1->x+p*(b2->x-b1->x);
t.y=b1->y+p*(b2->y-b1->y);
t.z=b1->z+p*(b2->z-b1->z);
return t;
}
if(b1->z==b2->z && a1->z!=a2->z)
{
p=(b1->x-a1->x)/(a2->x-a1->x);
t.x=a1->x+p*(a2->x-a1->x);
t.y=a1->y+p*(a2->y-a1->y);
t.z=a1->z+p*(a2->z-a1->z);
return t;
}
if(a1->x!=a2->x && b1->x!=b2->x && a1->y!=a2->y && b1->y!=b2->y)
{
p=((a1->x-b1->x)/(b2->x-b1->x)-(a1->y-b1->y)/(b2->y-b1->y))/((a2->y-a1->y)/(b2->y-b1->y)-(a2->x-a1->x)/(b2->x-b1->x));
t.x=a1->x+p*(a2->x-a1->x);
t.y=a1->y+p*(a2->y-a1->y);
t.z=a1->z+p*(a2->z-a1->z);
return t;
}
if(a1->y!=a2->y && b1->y!=b2->y && a1->z!=a2->z && b1->z!=b2->z)
{
p=((a1->y-b1->y)/(b2->y-b1->y)-(a1->z-b1->z)/(b2->z-b1->z))/((a2->z-a1->z)/(b2->z-b1->z)-(a2->y-a1->y)/(b2->y-b1->y));
t.x=a1->x+p*(a2->x-a1->x);
t.y=a1->y+p*(a2->y-a1->y);
t.z=a1->z+p*(a2->z-a1->z);
return t;
}
if(a1->x!=a2->x && b1->x!=b2->x && a1->z!=a2->z && b1->z!=b2->z)
{
p=((a1->x-b1->x)/(b2->x-b1->x)-(a1->z-b1->z)/(b2->z-b1->z))/((a2->z-a1->z)/(b2->z-b1->z)-(a2->x-a1->x)/(b2->x-b1->x));
t.x=a1->x+p*(a2->x-a1->x);
t.y=a1->y+p*(a2->y-a1->y);
t.z=a1->z+p*(a2->z-a1->z);
return t;
}
}
贴图函数:
void image(VECTOR3D *lt,VECTOR3D *lb,VECTOR3D *rt,VECTOR3D *rb,PIMAGE *image,float *wxy,bool s)//s为是否判断重合,0为判断重合,1为不判断重合
{
int w;
int h;
int tx,ty;
float wt=sqrt(pow(lt->x-rt->x,2)+pow(lt->y-rt->y,2)+pow(lt->z-rt->z,2));
float wb=sqrt(pow(lb->x-rb->x,2)+pow(lb->y-rb->y,2)+pow(lb->z-rb->z,2));
float hl=sqrt(pow(lt->x-lb->x,2)+pow(lt->y-lb->y,2)+pow(lt->z-lb->z,2));
float hr=sqrt(pow(rt->x-rb->x,2)+pow(rt->y-rb->y,2)+pow(rt->z-rb->z,2));
//计算4条边的长度
int i,j;
VECTOR3D a1;
VECTOR3D a2;
VECTOR3D b1;
VECTOR3D b2;
VECTOR3D t;
t.z=lt->z;
if(t.z<lb->z){
t.z=lb->z;
}
if(t.z<rt->z){
t.z=rt->z;
}
if(t.z<rb->z){
t.z=rb->z;
}
//求出最前端的点的z轴值
if(wt>=wb)
{
w=wt*1.1;//为了避免偶然漏算,所以乘个1.1
if(mode==0)
{
w*=l/(oz-t.z+l);
}
else
{
w*=l/(oz-t.z);
}
}
else
{
w=wb*1.1;
if(mode==0)
{
w*=l/(oz-t.z+l);
}
else
{
w*=l/(oz-t.z);
}
}
if(hl>=hr)
{
h=hl*1.1;
if(mode==0)
{
h*=l/(oz-t.z+l);
}
else
{
h*=l/(oz-t.z);
}
}
else
{
h=hr*1.1;
if(mode==0)
{
h*=l/(oz-t.z+l);
}
else
{
h*=l/(oz-t.z);
}
}
color_t* p=getbuffer(*image);
//定义图片指针
for(i=0;i<w;i++)
{
a1.x=lt->x+(rt->x-lt->x)*i/w;
a1.y=lt->y+(rt->y-lt->y)*i/w;
a1.z=lt->z+(rt->z-lt->z)*i/w;
a2.x=lb->x+(rb->x-lb->x)*i/w;
a2.y=lb->y+(rb->y-lb->y)*i/w;
a2.z=lb->z+(rb->z-lb->z)*i/w;
for(j=0;j<h;j++)
{
b1.x=lt->x+(lb->x-lt->x)*j/h;
b1.y=lt->y+(lb->y-lt->y)*j/h;
b1.z=lt->z+(lb->z-lt->z)*j/h;
b2.x=rt->x+(rb->x-rt->x)*j/h;
b2.y=rt->y+(rb->y-rt->y)*j/h;
b2.z=rt->z+(rb->z-rt->z)*j/h;
t=findIntersection(&a1,&a2,&b1,&b2);
//调用求交点函数
if(mode==0){
tx=ox+t.x*l/(l+oz-t.z);
ty=oy+t.y*l/(l+oz-t.z);
}
else
{
tx=ox+t.x*l/(oz-t.z);
ty=oy+t.y*l/(oz-t.z);
}
//判断成像方式并储存点在屏幕上的位置
if((*(wxy+tx+ty*getwidth())<t.z or *(wxy+tx+ty*getwidth())==oz)&&t.z<=oz or s)//判断是否重合
{
*(wxy+tx+ty*getwidth())=t.z;
putpixel(tx,ty,p[j*getheight(*image)/h*getwidth(*image)+i*getwidth(*image)/w]);
}
}
}
}
5、示例代码:
#include"found3d.h"//引用自制.h文件
main()
{
initgraph(450,300,INIT_RENDERMANUAL);
setcaption("3D贴图");
wxy=(float*)realloc(wxy,getwidth()*getheight()*sizeof(float));
//设置wxy长度
defineva(70);
//设置视角,成像模式为默认的摄像头成像方式
ox=(getwidth()+1)/2;
oy=(getheight()+1)/2;
//计算屏幕中心点坐标
oz=400;
emptywxy(wxy);
//“归零”wxy
PIMAGE a=newimage();
getimage(a,"3D贴图.png");
PIMAGE t=newimage();
VECTOR3D point[8];
int i,j,k;
for(i=0;i<2;i++)
{
for(j=0;j<2;j++)
{
for(k=0;k<2;k++)
{
point[i*4+j*2+k].x=50*pow(-1,i);
point[i*4+j*2+k].y=50*pow(-1,j);
point[i*4+j*2+k].z=50*pow(-1,k);
}
}
}
//定义图片和方块点坐标
while(1)
{
for(i=0;i<8;i++)
{
turn3d_y(&point[i],3);//自制旋转函数,绕y轴旋转
}
//依次旋转点
getimage(t,a,209,209,209,209);
image(&point[7],&point[5],&point[3],&point[1],&t,wxy,0);
getimage(t,a,209,0,209,209);
image(&point[6],&point[7],&point[2],&point[3],&t,wxy,0);
getimage(t,a,0,209,209,209);
image(&point[6],&point[4],&point[7],&point[5],&t,wxy,0);
getimage(t,a,209*2,209,209,209);
image(&point[3],&point[1],&point[2],&point[0],&t,wxy,0);
getimage(t,a,209*3,209,209,209);
image(&point[2],&point[0],&point[6],&point[4],&t,wxy,0);
getimage(t,a,209,209*2,209,209);
image(&point[5],&point[4],&point[1],&point[0],&t,wxy,0);
//在6个面上绘制图片
emptywxy(wxy);
//“归零”wxy,准备下一次绘制
delay_fps(30);
cleardevice();
//刷新屏幕
}
}
20240205_203124