3D游戏引擎系列五

3D引擎 同时被 3 个专栏收录
154 篇文章 6 订阅
97 篇文章 8 订阅
13 篇文章 31 订阅

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN课程视频网址:http://edu.csdn.net/lecturer/144

GPU本身也是一种硬件设备,它是嵌入在显卡里面的,以前显卡没有GPU芯片时,渲染材质都是通过CPU去处理效果一般,随着显卡性能的提高,把在CPU处理的顶点变换和矩阵变换放到GPU中去处理,增加处理速度。GPU编程语言比较流行的有三种:

一是基于OpenGL 的GLSL(OpenGLShading Language,也称为GLslang)

二是基于Direct3D 的HLSL(High Level ShadingLanguage)语言

三是NVIDIA 公司的Cg (C for Graphic)语言

其中GLSL和CG语言可以跨平台编程,可以同时在Windows系统和Linux系统上运行,而HLSL语言只可以在Windows平台上运行。对于客户端程序开发者来说,除了客户端逻辑开发,应该把GPU编程也好好学习一下,后面随着自己技能的提升,再学习3D引擎客户端最核心的技术算法方面的知识,对以后职位晋升非常有帮助。下面是我刚开始学习GPU编程时制定的目标,在这里拿出来分享给大家以供参考。

第一步学习目标:至少能看懂别人写的Shader代码,换句话说可以根据项目需求能够利用网上的Shader代码非常轻松的修改以满足自己项目需求;

第二步学习目标:学会调试Shader脚本文件,如何调试?因为Shader对于程序来说就是一个文本文件,不可以下断点,只能通过对其内部赋固定值或者逐步删除语句的排除法找问题,特别留意在Shader中的除法,要防止其被零除,还有在程序中尽量少加if else或者for,while循环等条件语句,对于GPU处理来说,因为它是多线程执行,运行速度很快,如果在Shader中加了多个条件语句,执行会打断其执行的流畅度,影响运行效率。

第三步学习目标:自己能根据项目需求写Shader或者使用Shader编辑器比如RenderMonkeyShader编辑工具开发。

以上三点也是我以前学习Shader编程时指定的学习目标,当然这个不是万能的,大家首先要把基础学好。对于Shader编程就是要把其语法以及经常使用的接口熟练掌握,至少掌握顶点着色器和像素着色器这些常用的技术,开始从最基本的Shader讲起。

学习Shader编程,我们从最基本的灯光渲染开始,灯光在游戏中使用的最多,在3D游戏中场景灯光能达到20-30个左右甚至更多,由于现在市面上的引擎都非常成熟,开发者只需要拖拖拽拽就可以实现一个简单的Demo,但是如果仅满足于使用,换句话说谁都会的技术体现不出自身的价值,要做到技高一筹就必须要研究其背后的技术,挖掘深层次的技术原理。下面就以实现常用的灯光为例,跟大家分享一下引擎使用的GPU编程技术:灯光的渲染是GPU编程中最基本的渲染,下面的案例都是基于Direct3D SDK图形库开发的,关于灯光Shader的编写,首先新建一个文本文件,修改扩展名为.fx,然后在其中添加文件内容如下,后面给大家详细解释:

float4x4  matWorldViewProj;
float4x4  matWorld;
float3 lightPos = float3(0, 60, -60);
float lightAttenuation = 0.01;

float4 mtlDiffuseColor = float4(1, 0, 0,1);

struct VS_INPUT
{
	float3 pos  :  POSITION;
	float3 normal  :  NORMAL;
};

struct VS_OUTPUT
{
	float4 pos  :  POSITION;
	float4 color  :  COLOR0;
};

VS_OUTPUT my_vs(VS_INPUT vert)
{
VS_OUTPUT vsout;
	
	vsout.pos = mul(float4(vert.pos,1),matWorldViewProj);
	
	float3 N = normalize(mul(vert.normal, matWorld));
	
	float3 worldPos = mul(float4(vert.pos,1),matWorld);
	float3 L = lightPos - worldPos;
	float d = length(L);
	L /= d; // normalize L
	
	float atten = 1/(lightAttenuation*d);
	float r = saturate(dot(N, L));
	vsout.color = mtlDiffuseColor * r * atten + float4(0.2,0,0,0);
	
	return vsout;
}

float4 my_ps(float4 color : COLOR0) : COLOR
{
	return color;
}

technique my_tech
{
	pass p0
	{
		VertexShader = compile vs_1_1 my_vs();
		PixelShader = compile ps_2_0 my_ps();
	}
}

这个Shader功能是游戏中聚光灯的实现,把重点代码给大家解释一下,第一行代码:

float4x4 matWorldViewProj

是在3D固定流水线讲过的世界矩阵,可视矩阵(相机矩阵),投影矩阵三者相乘得到的,这个值是通过外部C++传进来的,后面实现的C++编程中会讲到。

float4x4 matWorld,

表示的是世界矩阵

float3 lightPos = float3(0, 60, -60)

表示的是灯光的初始位置

float lightAttenuation = 0.01

表示的是灯光的系数

struct VS_INPUT

这个结构体表示输入参数

struct VS_OUTPUT

表示输出结构体参数,输入得到的结果是为输出服务的,下面函数:

VS_OUTPUT my_vs(VS_INPUT vert)

表示的是顶点着色器,返回的参数是一个输出结构体,内部形参是输入结构体,结构体内部进行的是顶点相关的计算。

float4 my_ps(float4 color : COLOR0) : COLOR

表示的是像素着色器,后面的冒号表示的是返回值的语义是Color,返回值表示的是float4四维向量,聚光灯就是一个圆锥体,跟手电筒类似,效果图如下所示:


假设点 P 处聚焦光源的光强颜色为 ,它的主发射方向为向量 U,空间中一点 Q 的聚焦光颜色可用如下的公式进行模拟计算。


其中  为3个Attenuation(衰减系数),d为点Q和P之间的距离,L为从Q指向P的单位向量。

再看下面的Shader语句,原理是把物体的位置通过世界、视口、投影矩阵变换到投影坐标系用于裁剪,使用的是GPU函数接口mul相乘得到。

vsout.pos = mul(float4(vert.pos,1),matWorldViewProj);

         normalize函数接口表示的是获取其世界坐标系的单位方向向量。

         float3 N = normalize(mul(vert.normal,matWorld));

         将物体从局部坐标转换到世界坐标系语句如下:

         float3 worldPos =mul(float4(vert.pos,1),matWorld);

下面两条语句的目的是获取世界坐标系中灯光与物体的距离

         float3 L = lightPos - worldPos;

         float d = length(L);

         关于灯光的计算公式如下所示:

         L /= d; // normalize L

         float atten = 1/(lightAttenuation*d);

         float r = saturate(dot(N, L));

         vsout.color = mtlDiffuseColor * r *atten + float4(0.2,0,0,0);

        

灯光的计算公式可以直接在Shader语句里面去执行,这样可以减轻CPU运算的压力,把在CPU计算的公式移到GPU中计算,提高了运算效率。注意一点,在游戏中任何物体都必须在世界坐标系中才能计算,只有在世界坐标系下计算才有意义。一个Technique可以包含多个pass,pass表示通道,使用的就是顶点和像素的计算,GPU运行时会起一个线程用于pass通道的处理,整个灯光的Shader就完成了。Shader文件必须与C++语言结合起来才能使用,如果没有C++代码的加载Shader文件是无法运行的。当然如果要查看Shader文件的运行效果,必须在特定的工具比如常用的工具RenderMonkey编辑器中查看,或者通过现有的图形库SDK加载Shader文件执行查看效果。接下来着手准备C++代码的工作,主要把核心的代码给大家展示一下。 C++代码中调用Shader文件的接口如下所示,其中ID3DXEffect和ID3DXBuffer都是Direct3D图形库提供的接口,在使用该接口时,需要在电脑中安装Direct3D SDK程序,同时在程序中引用Lib库以及include路径下的文件。

ID3DXEffect*pEffect = NULL;
ID3DXBuffer *pError= NULL;
         hr =D3DXCreateEffectFromFile(pD3DDevice,szFileName,
                   NULL,NULL,
                   dwFlag,
                   NULL,
                   &pEffect,&pError);
使用函数D3DXCreateEffectFromFile把Shader文件加载到内存里面,在Shader文件中定义了:
float4x4  matWorldViewProj;
float4x4  matWorld;
float3 lightPos =float3(0, 60, -60);
这些变量会通过C++传值给Shader文件,传值语句接口见加粗的部分:
         float t = timeGetTime() / 1000.0f;
         float r = 150;
         m_lightPos.x = sinf(t) * r;
         m_lightPos.y = cosf(t) * r;
         m_lightPos.z = sinf(t) * r;
 
         m_pEffect->SetFloatArray("lightPos",(float*)&m_lightPos,3);
 
         D3DXMATRIX matWorld;
         D3DXMatrixIdentity(&matWorld);
 
         D3DXMATRIX matWorldViewProj = matWorld* g_camera.getViewMat() * g_camera.getProjectMat();
 
         m_pEffect->SetMatrix("matWorldViewProj",&matWorldViewProj);
         m_pEffect->SetMatrix("matWorld",&matWorld);

m_lightPos表示位置可以在场景中旋转移动,直接调用Direct3D的接口SetFloatArray执行,其含义就是把m_lightPos的值以数组的形式传递给Shader文件执行,使用函数SetMatrix接口把矩阵的值传递给Shader文件执行。这样C++和Shader就有机的结合在一起了,运行效果如图:

程序运行后,场景中的灯光绕着物体旋转,在灯光旋转时可以见到灯光照亮的物体部分,当然也可以增加多个灯光照亮物体,这里只是简单的介绍其应用。


  • 4
    点赞
  • 1
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值