【图形学与游戏编程】开发笔记-基础篇5:光照与着色基础

(本系列文章由pancy12138编写,转载请注明出处:http://blog.csdn.net/pancy12138)

上一次的教程为大家补充了一些程序方面的知识,这些知识在现在以及以后的很多程序中都会经常的用到。当然,这些程序技巧也只是很少的一部分,如果以后学到更深层次的知识的时候我会继续对程序方面进行一些补充大笑。这篇文章将继续对图形学的讲解部分,这一次我们讲解的重点是光照与着色,也就是如何为3D空间中的物体进行上色的过程。

但是就在那一刻,张是非的眼中忽然一阵恍惚,他这才明白,原来色彩是假的,阳光才是真的。

——《我当鸟人的那几年》

我们在观察物体的时候,一般都会注意到物体的大概的颜色,因此我们在一开始在小学学画画的时候因为水平比较低,大都会一片一片的涂色,比如我们知道苹果是红色的,就一股脑的上一堆红色。或者天鹅是白色的,就一股脑的涂上一堆白色。这样虽然能够大概的画出一些东西,但是得到的图片一般都和真正的物体差的很远。如果大家之后再多练习的话就会了解一些简单的渐变色的技巧,不仅仅是用纯色来上色了,比如说苹果,会给暗的地方涂上暗红色,亮的地方涂上亮红色。当然这样画出的画会比之前好一点,得到一些插画或者漫画的风格。但是这依然和真正的物体差之甚远。这也就是为什么大家很容易看出来哪些是二次元的图片,哪些不是的原因。那么,如果我们拿一张照片来分析的话,会发现上面的物体基本上每一点的颜色都是不同的,想要画到这种境界显然是非常难的,那么既然大部分物体表面材料可以说是一样的,为什么会造成每一点的颜色都不相同呢?如果要回答这个问题,我们就得去了解颜色出现的本质,事实上如段首所说。所谓的颜色,其实是光照到物体之上然后反射到我们眼睛里面所导致的,由于物体上每一点所在的位置,以及其对应的表面法向量不同,最终反射到我们眼睛里的光线强度也就自然会有差别。这就是颜色的本质。在图形学里,我们不可能模仿画家绘画的技巧来进行上色,因为这是强模式识别和经验的产物,目前的计算机尚且不能模拟,并且这种绘制方法得到的图像的真实程度是有限的,即便是最优秀的画家也无法保证快速而完美的在再现他所看到的物体。因此我们只能一点点的对自然界的光照进行分析,借助分析所得到的一系列公式来对自然界中的物体进行上色操作。

那么,光照是一个怎样的过程呢,首先我们看看整个光照过程所用到的物理原型,也就是:光源,接受照射的物体,以及观察者。这三个东西可以直接决定最终眼睛得到的画面的各个点的颜色。而光线照射原理也很简单,大家高中物理肯定也学过:


上图展示了对于一个点而言,光照的反射与折射原理。这个简单的原理告诉了我们光照的传播过程,但是事实上,这只是一种最为简单的光照原理,并且即便是这个简单的原理,我们现在还有很多东西实现不了。要注意,这只是对于一个点,反射与折射也只有一次的情况得到的公式。真正要想算出一个物体的所有的正确颜色,我们需要对于每一个光源,每一个物体上的点,递归的计算好多次的反射与折射,如果大家实际上手编程的话,就知道这种复杂度根本就是一个天文数字,而这还不是真正的渲染方程,因为这里我们假设光源只是一个点,假设物体都是由一个硬的,不透明的表面组成的等等。事实上真的要深入的模拟光照情况的话,是一个非常复杂的过程。这也就是为什么如今的特效和CG都很烧经费。因为模拟这些光照需要进行超大量的运算,大量到需要借助一大批高性能计算机一起计算都要很长时间的地步。这种一心要模拟最优秀的渲染效果的领域一般称为离线渲染领域,这里所用到的算法和资源基本上都不是游戏能用的了的,因为这些算法(比如光线追踪,辐射度,SSS等)虽然渲染效果非常酷炫,但是不能在民用机上很快的得到结果。因此这些算法并不是我们要讲的重点.......

那么,游戏渲染所需要的光照模拟方法是神马样子的呢,这里我们所需要的光照算法称作实时渲染领域的算法。也就是必须能够在民用机器上,在1/30秒内得到渲染答案的算法。首先由于模型以及像素的基数过于庞大,目前所有的实时渲染算法必须要求是O(n)的算法,这也就是说多次折射,反射等效果都是不能进行的。或者说不能直接进行计算的,只能用别的近似算法进行模拟。我们先讲解最简单而且能够直接运算的算法,只有直接光照效果。也就是假设光源射出的光线不进行反射,也不进行折射,只对其第一次碰到的物体表面上的点产生效果。这样整个着色的算法就非常简单了,我们根据光源以及顶点的位置来进行一次光照计算,这一次计算我们可以得到两种反射到眼睛的效果,一个是漫反射效果,一个是镜面反射效果:


上图展示了两种不同的反射光的计算方法,其一是漫反射光,也就是入射光被平面均匀的散射到各个方向,并且所有方向的反射强度只取决于入射光与法线的夹角。这就很容易的得到了反射到我们眼睛里的漫反射光的颜色。然后是第二种,也就是镜面反射,这个复杂一点,入射光虽然也被散射到各个方向,但是强度却不是取决于入射光与法线夹角的,这个强度取决于反射光与视线的夹角,并且衰减速度极快,呈指数级别的衰减,大家平常在太阳下看到的车辆或者反光镜等物体上耀眼的亮斑就是这种反射造成的,而除非特别光亮的物体,其余物体的镜面反射都很不明显,颜色多半是取决于漫反射。下面是漫反射与镜面反射的公式:

漫反射公式:Idiffuse = L*M*Cos(theta)/dot(X(1,r,r^2));

其中L是光强,M是物体的漫反射系数,theta就是入射光与法线的夹角,X是衰减系数,这里我们可以看到其点乘了一个距离向量,也就是X的三个分量分别是二次系数,一次系数以及常数系数。这种写法是我经常用的写法,可能和别的书籍不太一样,不过衰减公式是不会变的。只是看怎么快速的实现它。

镜面反射公式:Ispecular  = L*M*(reflect(LightDir,Normal) * normalize(view))^w/dot(X(1,r,r^2));

这里reflect(LightDir,Normal)指的是光线沿法线反射后的反射光线方向,view是实现的方向,w是镜面反射的指数系数,代表镜面反射的特有衰减。

上述只是指出了当我们知道光源位置的时候,直接光照的计算方法。并没有提及光源是怎么传播光线的。下面我们来讲解一些常见的光源形式。一般来说实时渲染中用到最多的光源是点光源,方向光,聚光灯。这三种,前两者很好理解,点光源就是有一个点像四周发射灯光的光源,方向光就是沿着一个方向不做衰减的光源,而聚光灯比较复杂,主要就是类似于舞台灯光一样的圆锥状发射的光源,不仅仅沿距离方向衰减,沿着圆锥半径方向也会衰减。定义每一种光源所用到的数据结构是有一些不同的,下面是三种光源的定义:

struct pancy_light_dir//方向光结构
{
	//光照强度
	float4 ambient;
	float4 diffuse;
	float4 specular;
	//光照方向
	float3 dir;
	//光照范围
	float  range;
};
struct pancy_light_point//点光源结构
{
	//光照强度
	float4 ambient;
	float4 diffuse;
	float4 specular;
	//光照位置及衰减
	float3 position;
	//光照范围
	float  range;
	
	float3 decay;
};
struct pancy_light_spot//聚光灯结构
{
	//光照强度
	float4    ambient;
	float4    diffuse;
	float4    specular;
	//光照位置,方向及衰减
	float3    dir;
	float     spot;

	//聚光灯属性
	float3    position;
	float     theta;

	float3    decay;
	float     range;
};
上述就是三种光源的一些属性,前两者大家很容易理解我就不多说了,而聚光灯其实也就比点光源多个theta值和spot值用于判断圆锥照明区域的 半径方向衰减系数。这里我要提示大家一下,虽然三种光源的数据结构有所不同,但是我并不建议大家在写程序的时候把三种光源分别定义结构,因为这在引擎开发的后期及其不利于管理,所以这里我建议大家把光源的数据结构做一下统一,然后用一个参数来标记光源的类型:

struct pancy_light_basic
{
	//光照强度
	float4    ambient;
	float4    diffuse;
	float4    specular;
	//光照位置,方向及衰减
	float3    dir;
	float     spot;

	//聚光灯属性
	float3    position;
	float     theta;

	float3    decay;
	float     range;
	//光照类型
	uint4   type;
};
ok,现在我想大家已经对光源,光的直接传播有了很多的认识了,那么接下来我们就要开始根据这些知识进行最简单的光照着色了。注意上面我们只给出了漫反射光以及镜面反射光的计算方法,这里每个光源发出的光线中还有一种叫做“环境光”的分量,这个分量的来源大家可以想象一下在一间不开灯的房间里面,虽然没有任何光源,我们仍然能够看见东西,这就源于太阳光被别的物体反射的一系列的环境光在作祟。本来如果我们每个光都进行多次折射与反射的话,这个分量是不需要单独计算的。但是由于我们目前只能对各个光源进行一次直接照射的计算,所以环境光被单独的列了出来。模拟环境光在以往是一个很复杂的过程,并且曾经一度认为是实时渲染不可逾越的一步。直到后来cryengine2开发时的一个程序员提出了ssao算法,才勉强可以模拟。但是这属于比较高级的着色内容,我们会在后面为大家讲解,这里大家可以把环境光设定成一个常数。暂时不做模拟。下面就是最基本的光照计算shader:

首先是计算函数部分:

struct pancy_light_basic//聚光灯结构
{
	//光照强度
	float4    ambient;
	float4    diffuse;
	float4    specular;
	//光照位置,方向及衰减
	float3    dir;
	float     spot;

	//聚光灯属性
	float3    position;
	float     theta;

	float3    decay;
	float     range;
	//光照类型
	uint4   type;
};
struct pancy_material
{
	float4   ambient;   //材质的环境光反射系数
	float4   diffuse;   //材质的漫反射系数
	float4   specular;  //材质的镜面反射系数
};
void compute_dirlight(
	pancy_material mat,
	pancy_light_basic light_dir,
	float3 normal,
	float3 direction_view,
	out float4 ambient,
	out float4 diffuse,
	out float4 spec)
{
	ambient = mat.ambient * light_dir.ambient;         //环境光
	float diffuse_angle = dot(-light_dir.dir, normal); //漫反射夹角
	[flatten]
	if (diffuse_angle > 0.0f)
	{
		float3 v = reflect(light_dir.dir, normal);
		float spec_angle = pow(max(dot(v, direction_view), 0.0f), mat.specular.w);

		diffuse = diffuse_angle * mat.diffuse * light_dir.diffuse;//漫反射光

		spec = spec_angle * mat.specular * light_dir.specular;    //镜面反射光
	}
}

void compute_pointlight(
	pancy_material mat,
	pancy_light_basic light_point,
	float3 pos,
	float3 normal,
	float3 position_view,
	out float4 ambient,
	out float4 diffuse,
	out float4 spec)
{
	ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
	diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
	spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
	float3 lightVec = light_point.position - pos;
	float d = length(lightVec);
	ambient = mat.ambient * light_point.ambient;         //环境光

	float3 eye_direct = normalize(position_view  - pos);
	if (d > light_point.range)
	{
		return;
	}
	//光照方向          
	lightVec = lightVec /= d;
	//漫反射夹角
	float diffuse_angle = dot(lightVec, normal);
	//直线衰减效果
	float4 distance_need;
	distance_need = float4(1.0f, d, d*d, 0.0f);
	float decay_final = 1.0 / dot(distance_need, float4(light_point.decay, 0.0f));
	//镜面反射
	[flatten]
	if (diffuse_angle > 0.0f)
	{
		float3 v = reflect(-lightVec, normal);
		float spec_angle = pow(max(dot(v, eye_direct), 0.0f), mat.specular.w);
		diffuse = decay_final * diffuse_angle * mat.diffuse * light_point.diffuse;//漫反射光
		spec = decay_final * spec_angle * mat.specular * light_point.specular;    //镜面反射光
	}
}

void compute_spotlight(
	pancy_material mat,
	pancy_light_basic light_spot,
	float3 pos,
	float3 normal,
	float3 direction_view,
	out float4 ambient,
	out float4 diffuse,
	out float4 spec)
{
	ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
	diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
	spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
	float3 lightVec = light_spot.position - pos;
	float d = length(lightVec);
	//光照方向
	lightVec /= d;
	light_spot.dir = normalize(light_spot.dir);
	ambient = mat.ambient * light_spot.ambient;//环境光
	float tmp = -dot(lightVec, light_spot.dir);//照射向量与光线向量的夹角
	if (tmp < cos(light_spot.theta))//聚光灯方向之外
	{
		return;
	}
	if (d > light_spot.range)//聚光灯范围之外
	{
		return;
	}
	//漫反射夹角
	float diffuse_angle = dot(lightVec, normal);
	//直线衰减效果
	float4 distance_need;
	distance_need = float4(1.0f, d, d*d, 0.0f);
	float decay_final = 1.0 / dot(distance_need, float4(light_spot.decay, 0.0f));
	//环形衰减效果
	float decay_spot = pow(tmp, light_spot.spot);
	//镜面反射
	[flatten]
	if (diffuse_angle > 0.0f)
	{
		float3 v = reflect(-lightVec, normal);
		
		float spec_angle = pow(max(dot(v, direction_view), 0.0f), mat.specular.w);
		diffuse = decay_spot*decay_final * diffuse_angle * mat.diffuse * light_spot.diffuse;//漫反射光
		spec = decay_spot*decay_final * spec_angle * mat.specular * light_spot.specular;    //镜面反射光
	}
	
}
上面的代码为大家展示了如何计算各种不同光源下,物体受光照产生的颜色。接下来我们来看看整个着色的步骤:

float4 PS(VertexOut pin) :SV_TARGET
{
	pin.normal = normalize(pin.normal);
	pin.tangent = normalize(pin.tangent);
	float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
	float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
	float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
	float3 eye_direct = normalize(position_view - pin.position_bef.xyz);
	//float3 eye_direct = normalize(position_view);
	float4 A = 0.0f, D = 0.0f, S = 0.0f;
	float4 A1 = 0.0f, D1 = 0.0f, S1 = 0.0f;
	//compute_dirlight(material_need, dir_light_need[i], pin.normal, eye_direct, A1, D1, S1);
	for (int i = 0; i < 2; ++i)
	{
		//方向光(direction light)
		if (light_need[i].type.x == 0)
		{
			compute_dirlight(material_need, light_need[i], pin.normal, eye_direct, A, D, S);
		}
		//点光源(point light)
		else if (light_need[i].type.x == 1)
		{
			compute_pointlight(material_need, light_need[i], pin.position_bef, pin.normal, position_view, A, D, S);
		}
		//聚光灯(spot light)
		else
		{
			compute_spotlight(material_need, light_need[i], pin.position_bef, pin.normal, eye_direct, A, D, S);
		}
		//环境光
		ambient += A;
		//无阴影光
		diffuse += D;
		spec += S;
	}
	float4 final_color = ambient + diffuse + spec;
	return final_color;
}
上面就是这节课我们所需要更变的pixelshader,因为整个着色的流程我们在基础篇的第三节课已经讲过了,这一节课唯一不同的就是这个pixelshader是借助简单的光照方程来进行着色的(基础篇第三章只是简单的为立方体的各个顶点涂上了一些单一的颜色)。

由于代码和第三节课其实是差不多的,也就是改了一下pixelshader的着色方程,所以这次就不给大家贴代码了,个大家一个锻炼自我的机会,下面放出最终的光照效果图:



右边的高光效果就是镜面反射的效果,而左边的效果则是关闭镜面反射的漫反射着色效果,看渲染结果来理解还是很直观的。这节课大家只要能够理解了光照的作用以及书写方式就算是大功告成啦。直接照射虽然不难,但是它是最基础的着色效果,暂时还不能有更好的替代算法,所以大家还是要好好理解理解的大笑

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值