lihan님의 프로필Neil's Tech Space사진블로그리스트 도구 도움말

lihan

직업
지역
관심 분야

Neil's Tech Space

注意,本blog发布的文章均为原创,如须转载请注明来源
4월 26일

最近很忙,不讲技术,看截图吧

3billboard2m1UI2plant_tree

 

Image1: 基于Parallax的水面渲染

Image2: 基于billboard+批量渲染的草渲染

Image3: 无新技术(Instancing+billboard)

Image4: 新的UI,新的粒子系统

Image5: 基于Parallax的地形图渲染

 

1월 20일

我的第一个3D图形引擎-ProjectGaia截图

1

粒子系统演示图

 

2

批量渲染演示图

 

render-shuttle02  render-shuttle03

天空盒子,动画系统演示图

引擎特性:

基于.NET XNA, 底层为DirectX9 + HLSL

组件:

SkyBox

多种Camera

Animation

ParticleSystem

BSPSceneManager

OctreeSceneManager

LOD Terrain

ResourceManager

Shader Instancing

LightManager

InputManager

 

等全部完工之后开源,需要源代码者发email到blh@live.cn

Ogre场景管理之Octree源代码分析

由于本人的引擎ProjectGaia服务于08年创新杯的游戏项目 – 3D太空游戏,所以理所应当加入Octree(八叉树 – 已经周宁学长发帖介绍过)场景管理器.参考了无数Octree的代码,发现还是我们可爱的Ogre写的最好,于是狂看n千行代码,把精髓提取出来给大家共享.

鉴于我们游戏版教程又n久没有更新了,今天发一篇我对Ogre场景管理器之Octree源代码分析的笔记.

所有代码采用伪代码.

首先回顾一下Ogre场景管理的架构

clip_image002

Ogre以插件形式提供了多种场景管理器

1. BSP管理用于支持Quake系列

2. Terrain管理,优化大型的地形场景

3. Octree管理,用处多多,很适合太空游戏

4. 等等… (我只了解这几个:P)

以上所有的管理器都继承与SceneManager类,实现其提供的接口

并把与SceneManager挂接的SceneNode根据自身的特性进行管理.

进入正题: OctreeSceneManager – 名字好长啊,打字都麻烦

对八叉树的简介在附件中,如果不了解的请先看完附件,我就不废话了.

注意: 实际代码中的成员,方法数量是我下面提供的10倍以上..我只讲关键过程用到的成员和方法.

OctreeSceneManager : SceneManager

主要的方法:

FindVisiableObjs

WalkTree

UpdateOctreeNode

FindNodeIn(BoundingXXX)

主要的成员:

Root : Octree

WorldBox : BoundingBox

Octree

主要的方法:

GetChildIndex(BoundingBox)

GetCullBox()

主要的成员:

nodeList : List<OctreeNode>

box : BoundingBox

child : Octree[2][2][2]

parent : Octree

OctreeNode : SceneNode

主要的方法:

GetOctree

AddToRenderQueue

UpdateBound

主要的成员

LocalBoundingBox

这三者之间的关系如下:

OctreeSceneMgr包含一棵Octree的root, 一个Octree对象则包含了八个方向的子树(见上面的成员介绍), 一个Octree用链表管理了n多个OctreeNode. (补充: 由于OctreeNode继承与SceneNode, 看过我前面介绍的兄弟们都知道,SceneNode也是以树状结构组成的.但是此树非我们Octree中的树,因此在接下来的讲解中无视, 但要注意一点:OctreeNode的LocalBoundingBox则是由该SceneNode下面挂接的所有SceneNode的AttachedEntity (想像为一个或者多个3D模型)的boundingBox合并而成的…有点复杂了,多看几遍)

从程序执行的步奏来讲解:

首先,场景管理器为何物?答曰:cull,即剔除不可见的东东.

场景管理器的调用步奏基本如下: --- 参考燕良大牛blog…

1. SceneManager::_renderScene() 

2. SceneManager:: _updateSceneGraph从root node开始递归的调用了所有scene node的update,主要是计算了transform;

3.SceneManager::prepareRenderQueue 这里有一个Ogre场景管理的概念RenderQueue。粗略的看,这个类主要是为了把Objects按照材质分组,它还将管理对象的渲染优先权;

4.OctreeSceneManager::_findVisibleObjects
4.1 OctreeSceneManager::walkOctree
4.2 OctreeNode::_addToRenderQueue
可见这个操作利用SceneManager的空间管理算法来对所有的SceneNode进行了可见性判断,如果可能可见,则加入到RenderQueue中;

步奏也看了,那么开始分析具体的函数吧.从调用的顺序开始…

前三步是用于状态的update,和cull无关,从第4部开始才是关键…

_findVisibleObjects{

//没什么内容,初始化

//调用walkOctree

walkOctree(camera,renderQueue,root,visibilityInfo,false )

//搞定

}

walkOctree(camera,renderQueue,octree,visibilityInfo,foundVisible )

{

//顾名思义,遍历树

If(octree->NumOFNodes == 0)

Return //没有OtreeNode,也就是没有模型关节,根本谈不上可见判断

OctreeCamera::Visibility v = OctreeCamera::NONE; //不是伪代码

If(foundVisible){

//当为true的时候,说明这个节点的父节点已经判断为完全可见,所以不用费神了

v = OctreeCamera::FULL;

}

Else if(octree == root){

//根结点暴大,不可能全部visible

v = OctreeCamera::PARTIAL;

}

Else{

Box = octree->getCullBox() //Cull,剔除盒,大小比octree的boundingbox大125%

v = camera->getVisibilty() //通过摄像机的Frustum平截头体(形状和透视体一样,

//定义一个farClip面,一个nearClip,然后四点分别相连)大小判定

}

if ( v != OctreeCamera::NONE ) //如果不是完全不可见{

//处理当前Octree节点下面挂接的OctreeNodeList的可见性

//此处我会砍掉一些用于显示八叉树盒子线条的代码 – 无用

Foreach(OctreeNode n in octree){

//原文代码用的是迭代器

//对于每一个OctreeNode n,判断是否可见,AABB是轴对称包围盒的意思

Bool vis = camera -> isVisible( n -> _getWorldAABB() );

if(vis){

//Ogre很大很复杂,一下所有代码表示把该节点放到渲染队列

sn->_addToRenderQueue(camera,queue ,visibleBounds );

mVisible.push_back( sn );

if ( mDisplayNodes )

queue -> addRenderable( sn );

}

}

}

Foreach(Octree child in octree){

//对于当前octree的八个子结点

bool childfoundvisible = (v == OctreeCamera::FULL);

if(child!=null)

walkOctree … 递归调用

}

}

OctreeSceneMgr:UpdateOctreeNode ( OctreeNode * onode ) {

//由于挂接在八叉树上的OctreeNode:SceneNode的模型随时可能会移动,跑出当前的包

//围盒,所以我们需要重新计算传入的节点所在的Octree位置

If(该场景节点所挂接的Octree == null){

//没有被挂接在任何Octree上

//如果已经在root的范围以外,强行挂接的root上

if ( ! onode -> _isIn( mOctree -> mBox ) )

mOctree->_addNode( onode );

else

调用_addOctreeNode

}

If(该场景节点已经跑出所挂接的Octree){

从原来的Octree上remove掉

//没有被挂接在任何Octree上

//如果已经在root的范围以外,强行挂接的root上

if ( ! onode -> _isIn( mOctree -> mBox ) )

mOctree->_addNode( onode );

else

调用_addOctreeNode

}

}

//怎么添加一个新的场景节点 – 即挂接一个OctreeNode用于挂接模型

OctreeSceneManager::_addOctreeNode( OctreeNode * n, Octree *octant, int depth ){

首先获得要被挂接的节点n的包围盒

if ( ( 没有超过八叉树的最大深度) &&

被挂接的octree的包围盒大小是被挂接的节点包围盒的两倍 ){

Child = 根据节点n的包围盒大小计算其属于octree的哪个子结点

If(child == 0){

建立该节点

}

Else{

递归调用_addOctreeNode(n,child,++depth)

//作用在于找到改好能包围该场景节点的子树

}

}

}

好了,基本的流程就是这样,那么我要动工开始移植代码了.把Ogre的C++移植到基于XNA的C#中去…ZZZ…

有什么问题回帖讨论

HLSL翻译

前言:最近看了写HLSL(high level shader language)的东西.感觉很头痛,发现DX9 SDK里面有篇好文,翻译出来给大家看看.翻译的超级烂,如有错误,请指出.

HLSL Shaders

At a very high level, data enters the graphics pipeline as per-vertex data, is processed by each of the three stages, and generates pixel colors. Briefly, each stage of the pipeline performs a series of steps:

从很高的层面讲,数据以逐个顶点数据的形式进入图形流水线,并进行三个阶段的处理,最后生成像素颜色. 总的讲,每一个阶段执行以下步奏

  • Vertex processing. This includes vertex transforms, per-vertex fog, per-vertex material attributes, and per-vertex lighting.
  • 顶点处理,包括变化(译者注:偏移,伸缩,旋转),逐个顶点雾化,逐个顶点的材质属性,逐个顶点光照
  • Primitive processing. This is comprised of clipping, back-face culling, and per-primitive attribute evaluation in preparation for rasterizing.
  • 初始处理.包括裁减,隐藏面剔除,为光栅化做初始属性分析
  • Pixel processing. This is broken into two parts:
  • 像素处理,分类两部

Part 1 blends texture samples with per-pixel colors, completely replacing the fixed function multi-texture blender. This processing includes: iterating colors and texture coordinates, sampling textures, and blending texture samples with lighting and material colors.

步奏1 把texture sample和每个像素点混合,完全替换固定的多纹理混合.这个处理包括:轮训颜色和纹理坐标,采样纹理,混合纹理采样和光照及材质颜色.

Part 2 includes all the operations that determine what is written out to the frame buffer. This includes: alpha testing, depth testing, stencil testing, calculating per-pixel fog, alpha blending, dithering, and making gamma adjustments.

步奏2包含了所有为了确定什么应该写入到帧缓存的操作.包括通过alpha进行确定(译者注:透明度),用深度缓存进行确定(译者注:通过Z buffer), 使用stencil buffer进行确定(译者注:stencil buffer用于mask像素 – 实在不会翻译,掩模?)

A simple HLSL Shaders

Here is an example of a vertex shader that implements the minimum vertex processing. This shader transforms vertex position from object space to projection space and assigns per-vertex color from the vertex input.

这是一个顶点着色器的简单例子,他实现了最基本顶点处理. 着色器把顶点位置从物体空间转换为投影空间并通过vertex input对每个顶点进行赋值.

float4x4 mWorldViewProj; // World * View * Projection transformation

struct VS_OUTPUT

{

float4 Position : POSITION; // vertex position

float4 Diffuse : COLOR0; // vertex diffuse color

};

VS_OUTPUT Vertex_Shader_Transform(

in float4 vPosition : POSITION,

in float4 vColor : COLOR0

)

{

VS_OUTPUT Output;

// Transform the vertex into projection space.

Output.Position = mul( vPosition, mWorldViewProj );

// Output the diffuse color.

Output.Diffuse = vColor;

return Output;

}

Here is an example of a simple pixel shader. This shader simply copies the interpolated per-vertex color data, and outputs a pixel color.

这是一个简单的像素着色器的样例.他只是简单的复制了插值数据并输出像素值

float4 Pixel_Shader( VS_OUTPUT in ) : COLOR0

{

float4 color = in.Color;

return color;

}

These shaders looks very much like C code and contain one global variable, one data structure, and two functions. By browsing these shaders you can see that:

着色器和C代码看起来特别相似,包含了一个全局变量,一个数据结构和两个函数.你可以通过实例发现:

  • The shaders are readable.
  • 着色器是可读的.
  • The shaders use standard HLSL math functions like mul - HLSL, which does a matrix multiply.
  • 着色器使用标准的HLSL数学函数,例如mul-HLST用于矩阵乘法.
  • In addition to standard C syntax, HLSL introduces a few new data types like float4 and float4x4 that make it easier to map the shader to hardware shaders.
  • 出了C语法外,HLSL引入了一些新的数据类型,例如float4,float4x4来使着色器更简单的映射到硬件着色器.
  • HLSL defines semantics like POSITION And COLOR0 for inputs that are streamed into the shader from a vertex buffer.
  • HLSL定义了以下语法例如POSITION和COLORO用于作为从顶点缓存streamed到着色器的输入.

Vertex Shader Basics

When in operation, a programmable vertex shader replaces the vertex processing done by the Microsoft Direct3D graphics pipeline. While using a vertex shader, state information regarding transformation and lighting operations is ignored by the fixed function pipeline. When the vertex shader is disabled and fixed function processing is returned, all current state settings apply.

在运算中,可编程的顶点着色器取代了过去由M$的DX3D图形流水线中对顶点的操作.使用顶点着色器,那些关于变化,光照处理的信息不使用固定的函数流水线处理.当顶点着色器被屏蔽,则我们使用固定的函数处理流水线.(译者注:怎么感觉说的是废话呢)

As a minimum, a vertex shader must output vertex position in homogeneous clip space. Optionally, the vertex shader can output texture coordinates, vertex color, vertex lighting, fog factors, and so on.

顶点着色器至少要输出顶点在齐次空间中的位置.顶点着色器可以选择性输出纹理坐标,顶点颜色,顶点光照,雾化效果参数等等…

Pixel Shader Basics

Pixel processing is performed by pixel shaders on individual pixels. Pixel shaders work in concert with vertex shaders; the output of a vertex shader provides the inputs for a pixel shader. Other pixel operations (fog blending, stencil operations, and render-target blending) occur after execution of the shader.

像素着色器对每一个像素进行处理.像素着色器和顶点着色器协调工作.顶点着色器的输出作为像素着色器的输入.其余的像素操作(雾化混合,掩模操作,渲染目标混合)都发送在着色器执行之后.

Pixel Shader Inputs

For pixel shader versions ps_1_1 - ps_2_0, diffuse and specular colors are saturated (clamped) to the range 0 through 1 before use by the shader because this is the range of valid inputs to the shader.

对于像素着色器1.1 – 2.0版本,diffuse散射,specular反射光在使用之前被saturated(译者注:太专业了,不会翻译)到0~1的范围. 因为这才是规范合法的输入值.

Pixel Shader Outputs

For pixel shader versions ps_1_1 - ps_1_4, the result emitted by the pixel shader is the contents of register r0. Whatever it contains when the shader completes processing is sent to the fog stage and render-target blender.

For pixel shader versions ps_2_0 and above, output color is emitter from oC0 - oC4.

对于顶点着色器版本1.1到1.4,返回值是寄存器r0中的数据.之中的数据会被送到雾化处理阶段和渲染目标混合器.

对于顶点着色器版本2.0+,输出到oc0 – oC4.

待续………….

关于Mesh - 处女翻译

看到网上有个比较好的定义,翻译一下给大家(附英文以免大家看不懂我写的残缺不全的中文)
Vertex buffers define points in 3D space. Each element in the vertex buffer is defined by several attributes you can set. The only attribute you must set is the position of the vertex. Aside from that, there are many optional properties you can set, such as the color of the vertex, the texture coordinates, and so on. Which ones you will actually need to use is dependent on what you are trying to do with the mesh.
顶点缓存定义了三维空间中的点信息.顶点缓存中的每一个元素用几个属性来定义.最重要的属性当然是顶点的"坐标"或者称之为"位置".其他属性例如颜色,材质坐标可以根据实际情况来定义.
Index buffers "connect the dots" by selecting points from the vertex buffer. Every three indexes specified in the index buffer defines a single triangle to be drawn by the GPU. The order in which you select vertices in the index buffer tells the graphics card which way the triangle faces. A triangle which is drawn counter-clockwise is facing you, one drawn clockwise is facing away from you. Normally only the front of a triangle is rendered, so it is important to be sure that your triangles are setup properly.
索引缓存则负责把顶点缓存中的两点相连(定义线信息),每三个索引定义一个三角形,用于GPU绘制(定义面信息).(译者 - 也就是我啦 - 补充:所有的三维物体都是由很多个三角形构成的) 而我们选择索引缓存的选择顺序告诉了显卡三角形的朝向.我们定义:如果一个三角形是逆时针绘制的,则这个三角形面朝"你".通常只有面朝"你"的三角形才会被绘制.所以在设置三角形的时候我们需要注意:) - OGRE+建模软件(如3DMAX)实际上已经帮我们搞定了这些恶心的事情.
Though all meshes have a vertex buffer, not all meshes will have an index buffer.
所有的mesh都会包含顶点缓存,但不是所有的mesh都会保存索引缓存
Note that vertex and index buffers are usually stored in the video card's own memory, so your software can just send the card one simple, discrete set of commands to tell it to use those predefined buffers to render an entire 3D mesh in one go.
所有的顶点和索引缓存都保存在显卡的显存中.我们的程序所做的只是把简单的,离散的命令集发送给显卡,告诉显卡用这些预先定义好的缓存来渲染一个完整的三维Mesh.

OGRE教程 - 第一个Ogre程序

讲了这么久的空洞的教程,终于也该拿出点实际的代码了吧.看看我们Gamemaker版的人气,想哭啊…

好了,废话少说,进入正题.

相信大家都下载了OgreSDK, SDK里面提供了不少样例代码,有空运行运行,都是很帅的效果,看看代码,是不是有点难以置信这点代码就能实现这么帅的效果了? 呵呵,如果不行,那么让我们来解析一段最简单的样例代码吧.

Terrain这个sample应该是所有sample里面最简单的了,我们就从这个代码讲起吧.

先上一幅程序效果图.

clip_image002

是不是很帅啊.我保证那些没怎么看讲座的人看来这幅图以后也会想来学学Ogre了.

在所有的Ogre样例程序中,都使用了一个类OgreTutorialApp作为基类,该类提供了基本的接口用于封装win32应用程序,初始化Ogre等以前我们讲的基本知识,让我对该类的所有成员进行一一讲解.

class OgreTutorialApp

{

protected:

//这些private成员大家应该熟悉了,在这儿回顾一下

Root *mRoot; //根,顾名思义,一切Ogre程序的根

Camera* mCamera; //在所有3D引擎中普遍存在的类,不用说大家都知道是什么

SceneManager* mSceneMgr; //场景管理器,上次讲过了

OgreTutorialFrameListener* mFrameListener; //另外一个今天要讲解的类

RenderWindow* mWindow; //渲染窗口,在windows下面可以理解成对HDC封装

Ogre::String mResourcePath; //字符串类型保存资源文件的path

public:

//constructor

OgreTutorialApp()

virtual ~OgreTutorialApp()

///简单易用的启动程序的接口,再次调用其他函数例如:setup等

virtual void go(void)

//初始化root等private成员,调用configure等等

virtual bool setup(void)

//调用Ogre提供的Config窗口进行设置Config,例如分辨率,色深,底层API(OPENGL,DX)等

virtual bool configure(void)

virtual void chooseSceneManager(void)

//初始化camera

virtual void createCamera(void)

virtual void createFrameListener(void)

// pure virtual 需要在子类实现,在子类中实现例如光线等的创建

virtual void createScene(void) = 0;

//清理资源

virtual void destroyScene(void){}

//在这里创建默认的viewport,在子类中可以修改

virtual void createViewports(void)

/// Method which will define the source of resources (other than current folder)

virtual void setupResources(void)

/// Optional override method where you can create resource listeners (e.g. for loading screens)

virtual void createResourceListener(void)

/// Optional override method where you can perform resource group loading

/// Must at least do ResourceGroupManager::getSingleton().initialiseAllResourceGroups();

virtual void loadResources(void)

};

有了这个类,让我们看看我们的main函数有多么的简洁

INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )

{

// Create application object

TerrainApplication app;

try {

app.go(); //简单的接口调用…

} catch( Ogre::Exception& e ) {

MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!", MB_OK | MB_ICONERROR | MB_TASKMODAL);

}

return 0;

}

看完了大概,让我们看看这些东西的内部实现吧.

首先我们来看看Ogre程序是怎么启动的.

1. 首先调用setup初始化成员(这个setup可以看做是有实质意义的构造函数,因为我们默认的构造函数只是把所有的成员赋值为NULL,而setup则是负责new各个private成员)

这段代码应该很熟悉了,Root老大的初始化过程.在这里我们手工定义了所有的参数,即插件配置文件,资源文件,日志文件

mRoot = new Root(pluginsPath, mResourcePath + "ogre.cfg", mResourcePath + "Ogre.log");

2. 接下来我们要从资源文件中读取资源,调用setupResources

这是一个sample资源文件中的内容:

# Resource locations to be added to the 'boostrap' path

# This also contains the minimum you need to use the Ogre example framework

[Bootstrap]

Zip=media/packs/OgreCore.zip

# Resource locations to be added to the default path

[General]

FileSystem=media

FileSystem=media/fonts

FileSystem=media/materials/programs

FileSystem=media/materials/scripts

FileSystem=media/materials/textures

FileSystem=media/models

FileSystem=media/overlays

FileSystem=media/particle

FileSystem=media/gui

FileSystem=media/DeferredShadingMedia

Zip=media/packs/cubemap.zip

Zip=media/packs/cubemapsJS.zip

Zip=media/packs/dragon.zip

Zip=media/packs/fresneldemo.zip

Zip=media/packs/ogretestmap.zip

Zip=media/packs/skybox.zip

看不懂可以不管它.简单介绍一下,其中的Bootstrap组意味着只针对OgreSDK Sample的资源对象,它是一个可以随意起的名字.而General是系统默认的组并且始终存在,当你把资源添加到这个组,并不会创建新的资源组. FileSystem表示一个文件系统为基础的资源(理解为非zip文件);而Zip表示了资源路径是一个ZIP压缩文件的资源。

在我给的代码中setupResources函数看起来很怪异,用到了一个ConfigFile类,这个类类似与JAVA中的perperty类(本人JAVA实在很烂,基本不会,但是读过不少JAVA代码,仿佛记得有这个类),用于读取cfg文件中的section(例如general),type(例如Zip)及其key(例如media/fonts)的值.然后调用

ResourceGroupManager::getSingleton().addResourceLocation(archName, typeName, secName);

来添加管理资源.

由于我们使用的是OgreSDK提供的样例代码,resource.cfg文件里面包含了所有的sample程序需要用到的资源(也就是说调用了多个程序都要用到的资源文件),所以在我们自己写代码的时候,实际上是不需要那么复杂的.只需要使用默认的section: general,然后添加要使用的资源即可.或者直接在代码中硬编码:

ResourceGroupManager *rgm = ResourceGroupManager:;getSingletonPtr();

rgm->addResourceLocation(“../../media/packs/OgreCore.zip”,”Zip”,”Bootstrap”);

3. 完成了资源初始化,接下来就可以运行我们的程序的配置窗口了即调用configure. Ogre已经做了一个图形化的config窗口了,通过该窗口我们可以完成配置文件的生成.

4. 创建场景管理器. 在父类OgreTutorialApp我们完成了默认的管理器创建,而在应用中,我们会根据场景的不同(例如室内或者室外)选择不同的场景管理器.例如在Terrain这个sample里面,我们使用

mSceneMgr = mRoot->createSceneManager("TerrainSceneManager");

接下来的工作实际上是为子类提供标准的接口,在父类中这些函数什么都不做或者做一些默认工作.

5. createCamera(); 同样为虚函数,创建默认Camera

6. createViewports();

7. createResourceListener(); 所谓的resourceListener是用于在loading画面提供回调来显示进度的.

8. loadResources(); 读取资源,读取期间由resourceListener监听,在父类为空

9. createScene(); 纯虚函数,在子类中实现

10. createFrameListener(); FrameListener是真正实现每一帧绘图的类

完成这十步,我们的游戏也就可以运行了.

讲完了这个OgreTutorialApp父类,我们还需要讲FrameListener这个用于处理每一帧的父类OgreTutorialFrameListener

首先还是先看看这个类的成员

class OgreTutorialFrameListener: public FrameListener, public WindowEventListener

{

protected:

Camera* mCamera; //一个指向Camera的指针

Vector3 mTranslateVector; //在update中做运算的三维向量,作用在Camera上导致位移

//例如点UP键 mTranslateVector.z = -mMoveScale

RenderWindow* mWindow; //一个指向RenderWindow的指针

bool mStatsOn; //一个控制基本信息显示开关的成员,在sample中通过F按键触发

std::string mDebugText; //保存当前调试信息

unsigned int mNumScreenShots; //保存截图数

float mMoveScale; //由mMoveSpeed*时间片得来,用于移动Camera

Degree mRotScale; //同上,except是rotate,不是move

Real mTimeUntilNextToggle ; //延时用的,为了美观..在每次update中--

Radian mRotX, mRotY; //在鼠标事件中记录视角旋转,在moveCamera函数中使用,移动Camera视角

TextureFilterOptions mFiltering; //枚举型,BILINEAR, TRILINEAR, ANISOTROPIC,纹理插值类型

摘自MSDN:

clip_image003

Anisotropic

用作纹理放大或缩小筛选器的各向异性纹理筛选。此类型筛选弥补由于纹理多边形与屏幕平面之间的角度差异所造成的失真。

clip_image003[1]

Linear

用作纹理放大或缩小筛选器的双线性插补筛选。筛选后的纹理使用所需像素周围 2x2 区域内的“纹理像素”(单个像素纹理元素)的加权平均值。在 mipmap 级别间使用的纹理筛选器是三线性 mipmap 插补,在这种方法中,光栅化程序使用两个最近的 mipmap 纹理的纹理像素对像素颜色执行线性插补。

int mAniso; //Anisotropy值

int mSceneDetailIndex ; //设置Camera用的PolygonMode(渲染类型:Point, Wireframe, Solid)

Real mMoveSpeed; //表示单位移动的像素值大小,比如单击一次向前按键,移动多少像素

Degree mRotateSpeed; //同上,不过这里是通过移动鼠标控制视角转动的单位大小

Overlay* mDebugOverlay;

//OIS Input devices封装了win32丑陋的消息处理

OIS::InputManager* mInputManager;

OIS::Mouse* mMouse;

OIS::Keyboard* mKeyboard;

OIS::JoyStick* mJoy;

protected:

void updateStats(void) //用于更新文本状态,例如当前FPS率

public:

// Constructor takes a RenderWindow because it uses that to determine input context

OgreTutorialFrameListener(RenderWindow* win, Camera* cam, bool bufferedKeys = false, bool bufferedMouse = false, bool bufferedJoy = false )

//Adjust mouse clipping area

virtual void windowResized(RenderWindow* rw)

//Unattach OIS before window shutdown (very important under Linux)

virtual void windowClosed(RenderWindow* rw)

virtual ~OgreTutorialFrameListener()

//处理键盘事件

virtual bool processUnbufferedKeyInput(const FrameEvent& evt)

//处理鼠标事件

bool processUnbufferedMouseInput(const FrameEvent& evt)

//调用mCamera的接口实现移动,旋转摄像机

void moveCamera()

void showDebugOverlay(bool show)

// Override frameStarted event to process that (don't care about frameEnded)

bool frameStarted(const FrameEvent& evt)

bool frameEnded(const FrameEvent& evt)

};

看完了FrameListener讲解的同学应该知道,我们对游戏的操作都放到这个listener里面实现了.可以说这个Listener已经是一个小的游戏控制引擎了.

回想我见过的最经典的控制引擎当属homeworld,家园了. 其中缩放旋转的效果现在看来也就是对摄像机的旋转,平移.(然后,Homeworld的场景管理器效率肯定相当了得,否则不会达到那么好的帧率).

有了上面这个两个基类框架,使用他们来搭建一个简单的Ogre程序就容易多了.

在这儿我们再复习一下大体流程.

1. main函数启动我们的程序

2. 初始化资源,配置,插件等

3. 调用Framelistener进行每帧的渲染及其操作设备的IO控制

那么继续我们的Terrain sample的讲解.

有了框架,我们要做的就是在初始化资源的时候:

1. 初始化我们需要的资源,例如纹理等.在我们的sample实例中没有这一步,因为我们默认载入了所有sample需要的资源.

2. 选择合适的场景管理器,BSP或者是TerrainSceneManager甚至是自己实现的管理器

3. 设置特定的camera初始值

4. 创建合适的场景,例如加入光源等

5. 如果需要Loading界面,则加入相应的listener,在Framelistener的

6. 创建特定的FrameListener来实现渲染

在我们特定的FrameListener中,则需要实现以下内容:

1. 在frameStarted这个回调函数中加入需要绘制的特定内容,例如在我们terrain实例中,要实现地形追踪效果. 则需要计算当然Camera的垂直位置.

最后我讲讲地形追踪这个概念:

在FPS第一人称射击游戏中,玩家操作大兵爬坡上坎,视角根据地形高低移动,在游戏实现的时候实际上就是根据地形追踪算法来计算的.

即: 当前人眼垂直位置 = 地形高度 + 人眼距离地面位置

那么我们是怎么获得地形高度值的呢?

算法描述如下:

1. 设置射线原点为Camera当前位置

2. 设置射线方向为Y的负方向

3. 用RaySceneQuery类进行查询,返回该射线通过的所有物体,类型为WorldFragment

4. 如果返回为空,则我们已经陷到地表以下,则设置射线方向为Y的正方向

5. 则把Camera的Y坐标设置射线穿过的第一个物体的Y坐标,即地表

(注意,OGRE的Y方向默认向上指向天空)

今天就讲解到这里,有什么问题先看代码,看不懂发帖问我J

本来想在hello ogre中加入属于我们Gamemaker版的东东的,结果没有mesh,只好作罢,谁能免费用3DMax给我们做一个LOGO啊?

另外今天中午受到罗恺老师的启发,our next tutorial will be published in English.

OGRE教程 - 场景管理器

上次,我们讲了初始化OGRE,写完以后对自己很不满意,觉得写出来的东西没有什么实质内容,大家学不了什么东西.这次就讲点实际的吧.

首先讲解一点图形学知识.

基本变换

所谓变换,就是指物体的平移,旋转,缩放这几个最基本的操作. 而在3D的世界,这几个操作都是源于矩阵的乘法.(大家会不会问:平移怎么会是乘法呢?明明是加法啊?这个后面讲)

首先来讲讲矩阵的意义吧.

一个物体在一个三维坐标系中有一个固定的位置或者说是坐标 (x,y,z), 这就是一个1*3的矩阵.

平移

那么要使物体的位置发生变化,例如向X方向移动5个单位,实际上只需要简单的执行x+5即可,那么反映在矩阵上就是加上一个平移矩阵

clip_image002

旋转

clip_image004clip_image006

绕z轴旋转α度后的坐标转换为矩阵乘法:

clip_image008

缩放

缩放实际上就是让每个点的坐标乘以一个拉伸系数,如果把一个平面的四个点都乘以系数,那clip_image010么体系在平面上的效果就是平面被缩放了

讲完了最基本的三种变化以后,我讲讲变化的合成

例如,我们想让一个物体绕空间中任意轴旋转

clip_image012

那么我们需要做的要分几步:

第一步: 通过平移和旋转建立一个新的坐标轴,使新的坐标系的z轴和任意轴重合.

第二步: 在新的坐标系中,把物体绕新的z轴旋转(实际上就是绕k旋转)

第三步: 通过反向平移和旋转把物体恢复到以前的坐标系.

这个有点不好理解,大家多实践吧.

最开始我讲了在3D游戏中,一个物体往往既要平移,又要旋转等,而大量的矩阵乘法运算会极大的影响系统的速度,所以我们需要把矩阵乘法合并,即我们需要把平移矩阵和旋转缩放矩阵合并成一个矩阵来提高效率.可是我们的平移矩阵和源坐标执行的是加运算,怎么能合并呢?

我们的解决方案是齐次坐标系,把原先的三维矩阵扩展成四位从而实现平移矩阵和源矩阵相乘.

clip_image014齐次坐标转换:

那么在齐次坐标系下的平移,缩放,旋转分别如下:

clip_image016平移

缩放:

clip_image018

旋转:

clip_image020

合成三个操作只需要简单的把三个矩阵先乘起来即达到目的.

好,理论就到这里: 来点实际的.

今天我们讲OGRE的场景管理器

从数据管理上理解,场景管理器就是一个存放众多游戏单位的容器.从数据结构上讲,就是一个树的结构.之所以用树状结构,是因为在3D游戏中,对父节点的矩阵变化会应用到子结点.想像一个人在走动,人的身体这个根节点上挂接了手脚,人的平移会带动手脚的平移.

那么我介绍一个OGRE管理器的主要功能吧:

·在场景中创建和放置活动物体、灯光以及摄像机,并维护他们的在场景图中的变换。

·载入和布置世界地图(World geometry,与活动实体不同,世界地图是巨大且可以延伸的,通常情况下是不可移动的,比如一个完整的BSP地图 – 这个概念后面讲到)。

·对场景查询(Scene Queries)的支持,比如回答“在世界的某个原型空间内,都包含了那些物体”。

·剔除不可见物体并且将可见物体放入渲染队列。

·根据当前和渲染物体的透视图,对无方向的光源(Nondirectional Light)进行组织和排序(按由近到远)。

·设置并且渲染场景中的阴影。

·渲染场景中的其他物体,如背景和天空盒

·发送组织好的内容到渲染系统执行渲染

以前我们讲过,OGRE的优势是把场景管理器作为插件的形式载入.所以我们会和很多管理器打交道,但是有人会问,问什么需要很多管理器呢?不就是存游戏单位的容器吗?实际上,一个庞大的3D游戏,例如地牢围攻,即包含了广阔的平原,又包括了室内空间,而渲染这两种场景,需要不同的算法来进行渲染优化.

OGRE提供了两种管理器:

OctreeSceneManager,八叉树

这个以前znnren师兄在版上讲过

TerrainSceneManager

这个我还没有搞懂

这是OGRE场景管理器的树状结构图.

clip_image022

当我们在创建管理器的时候,我们会得到一个总的父节点Root Scene Node,用来挂接我们自己的节点.

讲完了层次关系,我们来讲讲坐标系这个概念:

在OGRE中,我们要和以下几个坐标系打交道:

物体坐标系:

如果你创建了一个物体(不论是通过3D模型工具产生还是手动构造),物体的顶点都会被放置在世界空间中。我们在这里举个例子:我们在这里是用3DS Max工具来进行模型的制作,在编辑场景的原点(0,0,0)建立一个边长为1的立方体,这样他的顶点都在类似(0.5,0.5,0.5)的范围内。我们所用的坐标,都是处在物体坐标系中的.当OGRE载入了这个物体之后,所有顶点的位置仍然何在3DS Max编辑空间内相同。换句话说,一个在3D工具中设置在点(1,1,1)的顶点,在导出到Ogre系统中,这个顶点相对于它挂接的场景节点的位置仍然是(1,1,1)。

当物体被挂接到场景节点上的时候,这个场景节点上的世界空间转换矩阵就会应用到这个物体上面的所有顶点上,从而把物体转换到世界空间的绝对位置,这也是确定物体在3D空间位置、缩放以及方向的原理。事实上这些转换并不是直接施加在物体顶点上面的,所以这些操作也不会影响物体上面所顶点到所挂接场景节点位置的距离。

本地空间

这个概念没什么可说的,只需要注意:对于物体的旋转和缩放来说,“本地空间”的概念就是绕着自己的轴旋转;相对于自己的中心点进行缩放。

父节点空间(Parent Space

想像地球围着太阳转动吧.太阳就是地球的父节点

世界空间(World Space

我们的摄像机所在空间就在世界空间中,相信这个概念也很好理解

其他的关于场景管理器的内容我们下次讲解

下面我给出一些代码

这是有Root创建场景管理器的代码,字符串表示创建的管理器类型标示

mSceneMgr = mRoot->createSceneManager(“TerrainSceneManager”);

接下来我们完善这个空的场景

void createScene(void)

{

   //设置环境光

   mSceneMgr->setAmbientLight(ColourValue(0.5, 0.5, 0.5));

   // Create a point light 创建点光源

   Light* l = mSceneMgr->createLight(“MainLight”);

   l->set Position(20,80,50);

//创建一个场景内容(注意我们建立的是内容,不是一个场景节点,因为OGRE的理念是场景 //内容和场景节点分离,从而提高扩展性),读取.mesh文件,上一章讲过mesh,可以理解成一个//素材

   Entity * ent = mSceneMgr->createEntity(“head”, “ogrehead.mesh”);

   // 设置材质

   Ent->setMaterialName(“Examples/EnvMappedRustySteel”);

   // 建立一个子场景节点,在该节点上挂接场景内容

   mSceneMgr->gerRootSceneNode()->createChildSceneNode()->attachObject(ent);

}

接下来我们从代码上来了解平移变化:

OGRE非常友善的封装了矩阵乘法(实际上从DirectX和OpenGL层面上就已经封装了,只不过我们还是要和矩阵打交道,因为我们没有场景管理器:p),所以你只需要说明你想要平移的单位,例如x方向100个单位,y方向10个单位. 下面的代码同样是在场景节点上进行变化,产生的结果是导致挂接在节点上的场景内容发生平移.节点和内容分离万岁

mSceneNode->translate(100.0, 10.0, 0.0);

其他的实例:

注意, TS_PARENT是声明使用的坐标系.顾名思义,PARENT表示使用父节点坐标系.说到这里,让我想起来刚才我们讲的矩阵变化的合成,如果没有场景管理器,你将自己解决所谓的LACAL和WORLD坐标系这些概念.你需要PUSH压栈,POP出栈你的矩阵来实现这一切.

// 对象绕自己的Y轴旋转一弧度,大约57角度

mSceneNode->yaw(Ogre::Radian(1.0));

// 对象绕父节点的X轴旋转一弧度,大约57角度

mSceneNode->pitch(Ogre::Radian(1.0), TS_PARENT);

// 对象绕世界的Z轴旋转一弧度,大约57角度

mSceneNode->roll(Ogre::Radian(1.0),TS_WORLD);

// 在X轴缩放两倍,其他轴不缩放

mSceneNode->scale(2.0, 1.0, 1.0);

 
사진(1/5)