|
|
第一篇
http://bbs.gameres.com/showthread.asp?threadid=93004
第二部分解决方案
http://pickup.mofile.com/8350495349182884
为了节约时间,我只记录重点信息和有关思考动机,细节方面可参阅相关资料。
第二部分、光照和Gouraud着色

图2-1、执行了光照运算和Gouraud着色的示例
一、灯光类型
点光源:具有颜色、位置、强度。并且随着距离变化二次反比衰减。我在模拟模型中使用一次反比衰减和
固定衰减一次常量。
typedef struct
{
VECTOR3 P; //position
COLORF C; //color
float I; //intensity
}POINTLIGHT;
平行光:具有颜色、方向、强度。没有衰减距离。强度始终为常量。
typedef struct
{
VECTOR3 V; //vector
COLORF C; //color
float I; //intensity
}INFINITELIGHT;
环境光:具有颜色、强度,没有方向和衰减距离。强度始终为常量。
typedef struct
{
COLORF C; //color
float I; //intensity
}GLOBALLIGHT;
聚光灯:具有平行光的方向和点光源的距离衰减,以及根据照射方向发生的角度衰减。(没有实现)
灯光列表:和多边形流水线同级别的结构体,用于存储有关的灯光信息。
typedef struct
{
vector< OINTLIGHT> ptls;
vector<INFINITELIGHT> infls;
GLOBALLIGHT global;
}LIGHTS;
环境光只有一盏,可以这样理解,所谓的环境光是某区域场景的混色光线的简单模拟。
二、光线加法、光线调制、Alpha混合、线性插值
光线具有红绿蓝三个分量。GDI对分量的存储是蓝绿红分量排序。这不符合广泛的约定。因此对颜色
重新定义如下:
Typedef DWORD COLORREF3;
#define RGB3(r,g,b) ((DWORD) (((r) << 16) | ((g) << 8) | (b)))
typedef struct
{
float r;
float g;
float b;
}COLORF;
以上添置了标记“3”以示区别。接下来描述的结构体用于存储浮点类型的颜色分量。取值从0.0f到
1.0f,之所以取这个范围是当分量之间进行乘法运算时,颜色值始终不会越界。这使得颜色乘法有意义。
类似的有镜面反射区域的余弦值的指数运算。正方向的余弦值始终在0到1之间,无论进行多少次次方运算
都不会越界。
可以对光线分量简单累加,这普遍适用于同性质颜色复合(如都是光线或者都是材质,注意光线和材
质相加没有意义,不过对材质的加法是假设材质信息即将被作为光线处理,如自发光的材质等)的多数情
况,当颜色值越界时简单地截断,因为设备无法表现更宽广的亮度值域。如果是光线对某种材质进行照射
的情况,因为材质是对光线的影响情况,而不是自发射的光源,所以没有可以累加的量,这时候调制运算
就可以获得有效的数据。浅色系的物体材质对光线的反射能力更强,进行调制乘法的结果也正说明这一点
。
对颜色三个分量同时乘于一个相同的标量,这会改变颜色亮度。如果一个固定配额。数个颜色根据该
配额比进行调制,最后累加,得出的结果是物体呈现透明化。这就是Alpha混合。
线性插值,即在一个指定的着色区域,从一种颜色变化到另一种颜色,需要对每个步进值计算颜色变
化率。平滑着色和纹理仿射等均使用了线性插值算法。
三、物体材质
我们理解的物体色,是指物体表面反射白色光线后呈现的颜色。具有以下几个分量:
环境光反射:材质的环境光反射分量决定了环境光对物体的影响程度。经常使用灰度值表示。
散射:即漫反射,物体的漫发射光线值不随观察角度变化。但是和光线射入方向和物体表面朝向关系
密切。

图2-2、散射强度受光线照射方向和法向量夹角影响
镜面反射:镜面反射是受物体表面朝向、观察者视线、光线入射方向三方面影响的。当观察者视线位
于光线反射方向上时,镜面反射强度最强,并依次余弦递减。典型的Blinn算法是根据这样的事实:当观
察者位于光线反射方向上,观察者视线方向跟光线入射方向的中间量为表面法线,并依次按中间值的偏移
递减。计算两个向量的中间量的运算量要比计算投影线直到计算出反射方向的运算量少。缺点是变化率和
准确地Phong模型并不一致。但看上去足够可信。
另外镜面反射材质还有一个镜面反射率的指数位标记。越大的反射率,对夹角余弦的次方运算更为高
次,衰减的速度快速下降,这样将会得到一个足够集中的反射区域,正如我们在现实生活中感受的那样。

图2-2a、k=13

图2-2b、k=138
自发光:和环境光线类似,不过实现要更加简单,只是一个物体发射光的参照量,如前所述,对于实
体光线,需要做的运算就是光线加法。
typedef struct
{
float r;
float g;
float b;
float a;
}AMBIENT;
typedef struct
{
float r;
float g;
float b;
float a;
}DIFFUSE;
typedef struct
{
float r;
float g;
float b;
float k;
float a;
}SPECULAR;
typedef struct
{
float r;
float g;
float b;
float a;
}EMISSIVE;
Ambient:环境光反射、Diffuse:散射、Specular:镜面反射、Emissive:自发光。
光照计算公式:

公式2、光照计算
对光照的理解重点在于对光线加法和调制等操作的实际使用上。当然,对向量运算的背景知识是必须
的,这不是问题,不是吗?最后,用不多的几个光源进行合理搭配,就可以创造出良好的游戏氛围。

图2-3、镜面反射模型

图2-4a、打开了环境光

图2-4b、打开了平行光

图2-4c、加入了点光源
四、执行程序结构
在讨论丰富的渲染模式之前,有必要对程序的结构有一个整体清晰的认识。并且,对着色、插值、纹
理、光栅化、裁剪、深度缓冲等问题的讨论将不再允许有时间对这方面的问题做一个全面而具体的阐述。

图2-5、程序执行框图
主程序承载窗口和调度各个部件的协作。引擎负责像素级图形数据的绘制和呈现工作。流水线因为其
重要性,所以从主引擎结构中独立出来,当然主观上我们依然可以认为它们是一体的。流水线处理线上的
数据,并最终转化为可用的多边形和色彩值,调用引擎的绘制函数写入后备缓存。最后,主程序调用引擎
的呈现功能,把内存中的图像拷贝到屏幕上。
初始化:
相机储存有裁剪面、视场角等信息
typedef struct
{
EULER E;
VECTOR3 pos;
float nearclip;
float farclip;
float portang;
}CAMERA;
这些信息被用于设定相机坐标转换相关矩阵和屏幕缩放因子。
物体模型信息是通过外部文件导入的,存在着各种各样的文件格式。我们目前对文件格式有如下需求
:1、ASCII形式存储,因为我们有时候需要打开修改数据。2、材质描述,这在光照和着色渲染的时候必
须用到。3、可选的顶点法线,因为顶点法线可以在模型载入的时候再计算,对于有限的多边形数,这种
开销是值得的,所以顶点法线不是必须的。4、嵌入的或者引用的纹理信息。在ASCII文本中嵌入二进制信
息是可行的。幸运的是,我第一次接触存储文件的时候,使用的msh支持以上各种情况。
MSHX1
GROUPS 1
MATERIAL 1 ; Cube
NONORMAL
GEOM 20 12
-2.060000 2.040001 2.033892 0.000000 0.000000
…
0 1 2
…
MATERIALS 1
White Matte
MATERIAL White Matte
0.941177 0.941177 0.941177 1.000000
0.745098 0.745098 0.745098 1.000000
1.000000 1.000000 1.000000 1.000000 1.280000
0.000000 0.000000 0.000000 1.000000
TEXTURES 0
该种文件格式可以在MilkShape 3D的导出功能中找到,另外,它不支持顶点色和线框色,颜色是在材
质中描述的。我写了这种文件格式的加载函数,读取了有关的顶点材质信息,以及预先计算了顶点法线。
并存放在OBJ结构体中。(我已经尽量避免了大量的代码引用,但是这些结构体信息是必须的)
typedef struct
{
AMBIENT ambient;
DIFFUSE diffuse;
SPECULAR specular;
EMISSIVE emissive;
}MATERIAL;
typedef struct
{
vector<VECTOR3> vbuf; //顶点列表
vector<VECTOR3> nbuf; //法线列表
vector<COLORF> cbuf; //法线光照
vector<int> ibuf; //顶点索引
MATERIAL material; //材质
}MODEL;
typedef struct
{
vector<MODEL> models;
EULER E;
VECTOR3 pos;
MATRIX m;
}OBJ;
上面列出了物体、模型组、材质等的结构体。值得注意的是:1、法线光照会存入-1.0f,直到该法线
被计算的时候才被填充有意义的数值,这样每次流水线周期,法线光照都应该仅计算一次。2、物体的变
换矩阵MATRIX m,之所以使用这个矩阵的理由是:无论如何这个矩阵都会被计算一次,在进入流水线之前
可以对这个自变换矩阵进行操作,而不是实际数据,在流水线的开始点处该矩阵就会被执行。这样,修改
的将是流水线上拷贝的物体数据而不是原始的数据。因为整个流水线环节主程序是无权干涉的,因此,传
入这个矩阵是有意义的。
流水线:
typedef struct
{
vector<POINTLIGHT> ptls;
vector<INFINITELIGHT> infls;
GLOBALLIGHT global;
}LIGHTS;
typedef struct
{
vector<OBJ> objects;
TRIANGLE trianglist[30000];
}PIPELINE;
当前不支持聚光灯,所以灯光列表没有这个项目。流水线结构体放置了物体列表和多边形数组。这是
可以理解的,因为数组排序速度更快。
SetPos(teapot.pos,0,0,20);
SetEuler(teapot.E,0,-10,0);
teapot.m=MI*rotationy(angle);
pipeline.objects.push_back(teapot);
这是放置物体到流水线的示例
POINTLIGHT ptl;
EMISSIVE& emi=ball.models[0].material.emissive;
COLORF c={emi.r,emi.g,emi.b};
ptl.C=c;
ptl.I=30.0f;
ptl.P=ball.pos;
lights.ptls.push_back(ptl);
放置点光源到光源表的示例。其中较为有趣的部分是:我读取了场景中一个自发光球体的自发光信息
,并把它用于光源上,球体的位置信息也被用来描述点光源的位置,这样,我们得到了一个“真正发光”
的自发光体,至少看起来是:

图2-6、运动的球体“发射”了光线
最后用一段流水线代码来结束程序结构的讨论,用语言完整描述它们的方式是很糟糕的。
int pipelinerender(const CAMERA& cam)
{
MATRIX cm=MI; //相机变换矩阵
VECTOR3 campos=cam.pos;
cm=cm*translation(-campos.x,-campos.y,-campos.z);
cm=cm*MT(objecttoinertial(cam.E));
//透视投影
cm=cm*cameratoview();
VECTOR3 v0,v1,v2,u,v,n,vavg,normal0,normal1,normal2;
VERTEX a,b,c;
COLORF color0,color1,color2;
MATRIX m; //物体到世界变换矩阵
MATRIX mi=MI; //物体到惯性变换矩阵
MATRIX mw=MI; //惯性到世界变换矩阵
int index=0;
for(int p=0;p<pipeline.objects.size();p++)
{
OBJ& obj=pipeline.objects[p];
vector<MODEL>& models=obj.models;
//物体自变换和转换到世界坐标
mi=obj.m*objecttoinertial(obj.E);
mw=translation(obj.pos);
m=mi*mw;
for(int g=0;g<obj.models.size();g++)
{
for(int i=0;i<models[g].ibuf.size();i+=3)
{
v0=models[g].vbuf[models[g].ibuf]*m;
v1=models[g].vbuf[models[g].ibuf[i+1]]*m;
v2=models[g].vbuf[models[g].ibuf[i+2]]*m;
u=v1-v0;
v=v2-v0;
n=VCross(u,v);
n=VNormal(n);
//背面消除
if(backfaceclear(n,v0,cam.pos))
continue;
//对法向向量只旋转,不位移
normal0=models[g].nbuf[models[g].ibuf]*mi;
normal1=models[g].nbuf[models[g].ibuf[i+1]]*mi;
normal2=models[g].nbuf[models[g].ibuf[i+2]]*mi;
//执行光照
/*
vavg=(v0+v1+v2)/3;
color0=GetFaceColor(n,vavg,cam.pos,models[g].material,lights);
color1=GetFaceColor(n,vavg,cam.pos,models[g].material,lights);
color2=GetFaceColor(n,vavg,cam.pos,models[g].material,lights);
*/
if(models[g].cbuf[models[g].ibuf].r==-1.0f)
models[g].cbuf[models[g].ibuf]=GetFaceColor
(normal0,v0,cam.pos,models[g].material,lights);
if(models[g].cbuf[models[g].ibuf[i+1]].r==-1.0f)
models[g].cbuf[models[g].ibuf[i+1]]=GetFaceColor
(normal1,v1,cam.pos,models[g].material,lights);
if(models[g].cbuf[models[g].ibuf[i+2]].r==-1.0f)
models[g].cbuf[models[g].ibuf[i+2]]=GetFaceColor
(normal2,v2,cam.pos,models[g].material,lights);
color0=models[g].cbuf[models[g].ibuf];
color1=models[g].cbuf[models[g].ibuf[i+1]];
color2=models[g].cbuf[models[g].ibuf[i+2]];
//到相机坐标的转换
v0=v0*cm;
v1=v1*cm;
v2=v2*cm;
//相机空间裁剪,这步操作可以提前到光照计算之前,但需要保留变换前向量
if(cliptriangle(v0,v1,v2))
continue;
pipeline.trianglist[index].v0.v=v0;
pipeline.trianglist[index].v1.v=v1;
pipeline.trianglist[index].v2.v=v2;
pipeline.trianglist[index].v0.color=color0;
pipeline.trianglist[index].v1.color=color1;
pipeline.trianglist[index].v2.color=color2;
index++;
}
}
}
//排序
qsort(&pipeline.trianglist,index,sizeof(TRIANGLE),tricompare);
LockSurface();
for(int i=0;i<index;i++)
{
a.v=pipeline.trianglist.v0.v;
b.v=pipeline.trianglist.v1.v;
c.v=pipeline.trianglist.v2.v;
a.color=pipeline.trianglist.v0.color;
b.color=pipeline.trianglist.v1.color;
c.color=pipeline.trianglist.v2.color;
VDTriang(a,b,c);
drawcount++;
}
UnlockSurface();
return index;
}
int tricompare(const void* a,const void* b)
{
TRIANGLE t0=*((TRIANGLE*)a);
TRIANGLE t1=*((TRIANGLE*)b);
float z0avg=0.33333f*(t0.v0.v.z+t0.v1.v.z+t0.v2.v.z);
float z1avg=0.33333f*(t1.v0.v.z+t1.v1.v.z+t1.v2.v.z);
if(z0avg>z1avg)
return -1;
if(z0avg<z1avg)
return 1;
if(z0avg>z1avg)
return 0;
}
五、平滑着色,Phong或者Gouraud
最近我一直在考虑一个问题,如果把平滑着色的代码粘贴到文档编辑器中,它将占据几页的篇幅?答
案是13页。我花了几天时间慢慢完整这个算法。第一步我使用浮点斜率和Alpha混合插值,每秒可以执行
62000多个多边形。第二步我修改了多边形横向插值的内循环算法。使它不必执行乘法运算,而仅进行光
线加法。为此必须给它指定浮点颜色值。每秒计算的多边形量提高很有限,仅为63000个。第三步我放弃
了浮点颜色增量,而采用类似Bresenham画线算法的误差累积量修正。对于颜色值大于步进值的计算我置
入了循环累加。这次表现得很不错,有90000个多边形。后来我对浮点的斜率修正很不满意,决定用类似
的做法修改它,于是第四步我完成了整个函数的定点值计算,它处理了10200个多边行。我还发现了一些
优化的地方,显存位置可以通过累加计算获得,对特殊多边形区别处理。

图2-7、对顶点色进行插值计算
我打算对这种算法起了一个别名,uvw三角形光栅化算法。因为整个过程都是围绕三角形三个顶点的
位置关系来进行的。除此以外都是标准的插值算法。我总是假定U是最上端的顶点。V和W的位置关系在进
行x横向颜色插值的时候起作用,我们总是从x值较小的位置开始光栅化,但是较大的V侧的x值使得我们必
须从W侧的颜色开始插值。
Bresenham画线算法测试x、y方向的步进距离,并以较大的步进方向作为累加误差的轴向,而误差值
和较小的步进方向关联起来。

图2-8、利用x和y的步进值计算误差
如图所示,我们在x方向上持续画点,初始y方向的值为0,每在x方向上绘制一个像素,采用一个误差
值存储y步进量的累计值,当误差值大于x步进值,y方向的数值增加1个像素单位,并扣除。这样,当我们
绘制完x方向的全过程,在y方向也进行了等比例的长度修正。可能是如下的代码:
if(lx>=ly){
err=0;
for(i=0;i<=lx;i++){
DrawPixel(curx,cury,0x000000);
if(err>=ly){
err-=lx;
cury+=y_inc;}
err+=ly;
curx+=x_inc;}}

图2-9、y步进值比x步进值大的情况
考虑这样一种情况,如果y方向的步进值大于x方向的步进值,这时候需要对y方向执行步进操作,利
用误差值修正x方向的值。在画线任务中,这不是问题。但是在光栅化三角形的时候,情况有什么变化?

图2-10、光栅化三角形需要顾及V侧和W侧的斜率变化
我们可以使用类似的算法完成U到W的x方向偏移,因为x方向步进量相对于y方向的要小。但是U到V的
斜率变化则超出了我们的预计,每移动y方向一个像素,x方向都会有几个单位像素的变化。可以逆向思考
,如果在x方向偏移了这几个像素,导致了y发生一个像素的偏移,则可以置入内循环来叠加误差,并在误
差达到y发生偏移的量度之前退出,如此往复。代码可能如下:
if(Vloop==false)
{
if(Verr>Ylen1)
{
xV+=Vinc;
Verr-=Ylen1;
}
Verr+=Vstep_;
}
else
{
while(Verr<Vstep_)
{
xV+=Vinc;
Verr+=Ylen1;
}
Verr-=Vstep_;
}
前半段是典型的Bresenham算法,后半段使用了一小段的Bresenham算法倒置。循环的条件不再是主动
轴的跨度,而是引起被动轴发生修正的主动轴跨度。当然这个内置的倒置算法的主被动关系是相反的。即
主动轴依然是y轴而不是Bresenham算法认为的x轴。
在进行这个光栅化算法的完善过程中,遇到的唯一障碍是,当V侧或者W侧发生斜率变化时,x的当
前点已经可能产生了1个像素的误差,若对该x值再进行计算,则以下的点均可能往某个方向偏移了一个像
素。其导致的实际效应如图所示:

图2-11、糟糕的像素误差
我相信每个或多或少遇到此类问题的人都会马上取消他们的周末行程。因为对于误差的出现我们已经
熟视无睹了,特别是定点运算。但是有时候多边形最终呈现的结果很小,几个像素大小而已,这种误差必
须避免。唯一的做法就是使用斜率变化处的顶点坐标作为起点坐标。并清除之前的偏移误差量。
if(minY==Yuv)
{
xV=v.x;
Ylen1=abs(Yvw);
Vstep=Xvw;
Vstep_=abs(Vstep);
Vinc=Vstep>0?1:-1;
Vloop=false;
Verr=0;
if(Vstep_>Ylen1)
Vloop=true;
}
我将此称为“xV=v.x”启示。

图2-12、xV=v.x启示的结局
颜色插值有一致的算法,差别只是,xV到xW的变化量为主动轴向,像素色的变化为被动轴向。像素色
分量从0到FF,每个像素增量单位为1,对应每个颜色的增量单位1,并进行轴向长度比较。
了解了基本的插值算法后,应该开始了解Gouraud如何在多边形渲染中工作的了。
如前所述,Gouraud采用顶点颜色插值。这需要我们计算顶点的颜色值。有一个问题是,一个顶点通
常被几个多边形共用,而多边形经常是不共面的,因此顶点处的法向量应该是共点的几个多边形平面的面
法线的均值,这样才能返回可信的光照结果。
向量的位置没有意义,只有它的方向才有意义。当然,所谓的方向固定意味着向量起点和终点之间位
置关系的不变性。因此,事先计算各个顶点的法向量,在流水线周期内,利用旋转变换矩阵对法向量进行
操作,便可以送入光照管道。光照的位置信息是和顶点相对坐标原点的位移联系的。这就提出了一个要求
,光照管道内的所有向量信息都应该是归一化的。

图2-13、顶点法线受周围多边形面法线影响
用于计算顶点法线的代码段(Code Section):
vector<VECTOR3> normals;
VECTOR3 u,v,n,v0,v1,v2;
COLORF c;
for(int i=0;i<model.vbuf.size();i++)
{
normals.clear();
//寻找具备该顶点的多边形
for(int j=0;j<model.ibuf.size();j+=3)
{
if((model.ibuf[j]!=i)&&(model.ibuf[j+1]!=i)&&(model.ibuf[j+2]!=i))
continue;
//计算多边形面法线
v0=model.vbuf[model.ibuf[j]];
v1=model.vbuf[model.ibuf[j+1]];
v2=model.vbuf[model.ibuf[j+2]];
u=v1-v0;
v=v2-v0;
n=VCross(u,v);
normals.push_back(n);
}
//所有法线相加
for(int j=0;j<normals.size();j++)
n=n+normals[j];
n=VNormal(n/normals.size());
model.nbuf.push_back(n);
SetColorf(c,-1.0f,-1.0f,-1.0f);
model.cbuf.push_back(c);
}
最后说明的一个原则是:在设计算法的时候,如果有进行迭代,应在迭代数据产生增量前使用数据,
在使用数据前计算溢出和裁剪。次序上的混乱经常是未知错误的根源。如上面的斜率变化应该属于溢出状
态,即y扫描线已经到达了限界,必须在使用数据前修改部分数据。而进行增量前使用数据则容易理解。
我们不应该在0扫描线未绘制的情况下把扫描线移动到了1,这样我们将缺乏0扫描线和增加了n+1扫描线。

图2-14、在没有纹理贴图参与的情况下,人物外形已经很逼真了
六、一个示例代码
使用为数不多的自定义代码可以创建高效灵活的执行程序,这是引擎的主要功用。它提供了大量的按
约定模式组织的重用代码段和函数集。特别是,经过封装,可以利用显见的和容易理解的主程序逻辑来驱
动引擎,达到预想的效果。
示例代码仅提供这些必须的信息:模型文件位置、相机信息、环境灯光,渲染器则提供:物体位置,
光源信息。此外的一切任务全部在引擎中执行。在使代码简约、可读性强以及格式命名等方面进行了各种
权衡和取舍。为了使结构体名尽量简单,我几乎都是使用它们直接的明文形式,这可能会导致重名问题,
在某种必要的情况下,可以加入名称空间(Namespace)。
CAMERA cam;
OBJ wall,robit;
OBJ ball;
OBJ ball1;
int CustomInit(HWND hWnd)
{
//设定欧拉相机位置
SetPos(cam.pos,0,50,-90);
//设定欧拉相机方位
SetEuler(cam.E,0,30,0);
cam.nearclip=20;
cam.farclip=300;
cam.portang=90;
SetCamera(cam);
Load_msh(_T("彩色墙面.msh"),wall);
Load_msh(_T("机器人-武器.msh"),robit);
Load_msh(_T("ball.msh"),ball);
Load_msh(_T("ball1.msh"),ball1);
//设定环境光
setupgloballight(rgb3tofloat(0xFFFFFF),0.42f);
return 0;
}
int CustomRender(HWND hWnd)
{
static float starttime=(float)GetTickCount();
static float timedelta=0.0f;
timedelta=GetTickCount()-starttime;
//变换角度
static float angle=0;
angle=ANormal(timedelta/1000.0f*100.0f);
SetPos(wall.pos,0,0,0);
SetEuler(wall.E,0,0,0);
wall.m=MI*rotationy(-5);
pipeline.objects.push_back(wall);
SetPos(robit.pos,0,10,0);
robit.m=MI*rotationy(-angle);
SetEuler(robit.E,0,0,0);
pipeline.objects.push_back(robit);
SetPos(ball.pos,0,0,0);
SetPos(ball1.pos,0,0,0);
ball.pos=ball.pos*translation(30,0,0)*rotationy(angle);
ball1.pos=ball1.pos*translation(0,30,0)*rotationx(angle)*translation(0,8,0);
SetEuler(ball.E,0,0,0);
SetEuler(ball1.E,0,0,0);
ball.m=MI;
ball1.m=MI;
pipeline.objects.push_back(ball);
pipeline.objects.push_back(ball1);
POINTLIGHT ptl;
EMISSIVE& emi=ball.models[0].material.emissive;
COLORF c={emi.r,emi.g,emi.b};
ptl.C=c;
ptl.I=80.0f;
ptl.P=ball.pos;
lights.ptls.push_back(ptl);
EMISSIVE& emi1=ball1.models[0].material.emissive;
COLORF c1={emi1.r,emi1.g,emi1.b};
ptl.C=c1;
ptl.I=80.0f;
ptl.P=ball1.pos;
lights.ptls.push_back(ptl);
INFINITELIGHT infl;
infl.V=VSet(0.0f,0.0f,1.0f);
SetColorf(infl.C,1.0f,1.0f,1.0f);
infl.I=0.3f;
lights.infls.push_back(infl);
int trianglecount=pipelinerender(cam);
VDEnterGDI();
TCHAR buf[55];
_stprintf(buf,_T("%.3fFPS"),ShowFps());
VDText(50,50,buf);
_stprintf(buf,_T("多边形:%d"),trianglecount);
VDText(50,70,buf);
_stprintf(buf,_T("每秒多边形:%d"),drawcountpersecond);
VDText(50,90,buf);
VDLeaveGDI();
return 0;
}
七、对示例代码的维护
随着理解程度的深入和内容的持续扩充,之前一些演示代码的格式可能过时的,最简单的做法是,每
个阶段的引擎系统和相关演示对应,并作为一个整体存储。这样就不必为了修改那些过时的结构体和编写
风格而写了很多不必要的维护代码。事实证明这是合理而必要的。在进行最新内容的扩充时将尽量避免对
结构体的改变,若不得不这样做则将在阶段代码中统一维护。
八、实用的工具集合
三维建模、格式转换:MilkShape 3D 1.710
几何图形绘制:几何画板4.07
电子书制作:pdfFactory Pro 3.17
图标制作:Microangelo Toolset 6
VC助手(提供完整的智能提示):Visual AssistX 10.1.1418
公式编辑:MathType 5.2
以上工具均可在网络上获取。
|
|