分类目录归档:图形学

关于颜色预乘(premultiply)

引言

在许多纹理打包器中都有一个颜色预乘选项,但是我在第一次使用的时候,并没有特别理解这个句话的意思,理解上只是混合算法不同,并不理解引入这个的目的如何。

从理解上来说,将本来分离的RGBA四个因子重新预乘起来,是一个很奇怪的事情,特别是在早期,我一直觉得不预乘扩展性应该还好点,反正我可以在运行时再乘,也不是很在乎多一个乘法的效率问题。

为什么要引入预乘

这个就是我在本次阅读中学习到的点,我从这个文章中得到的启示,这个是spine论坛中的一篇文章,从目的到混合算法都说得很明确,我这里简单说明下。

传统的后乘alpha算法在shader或者混合配置中应该是这样的。

blend(source, dest) = (source.rgb * source.a) + (dest.rgb * (1 – source.a))

而预乘算法是这样的

blend(source, dest) = source.rgb + (dest.rgb * (1 – source.a))

从感受上来说,只是程序员为了效率引入的东西,而实际上他还解决了以下几个问题。 ·解决纹理比例缩放映射产生的颜色错误问题 ·可以和其他纹理一起正常混合而不打破批次渲染

本文最重要的目的是讲解第一点,为什么会出现颜色的错误问题。当一个图片素材被引擎放置在屏幕上时,往往不是1:1映射的,有可能因为在世界坐标的不同位置,映射到屏幕可能有不同的缩放比例。

比如有linear,nearest等算法可以将缺失的像素补全。这时候这些缺失像素的计算,会因为alpha因子的本身也会被平均而导致RGB因子被缩放两次,进而会产生一个黑色的边缘。

而实际上我们希望的结果应该是alpha因子RGB值只被平均一次,进而有需求就是将RGB值在素材中就被预乘。

关于瓷砖贴图的过渡(tile)

引言

瓷砖在游戏开发中的作用可以说非常巨大,哪怕在现在的2016年,依旧有非常多的游戏地表都是用有限的瓷砖拼起来的,除了id的mega texture以外,这里面的原因恐怕是不言自明的,因此我这段时间主要研究了一下这里面的一些技术问题,当然该问题跟美术相关性也比较大。

最早的瓷砖游戏

早期在红白机的几乎所有游戏都是瓷砖的,但是其中印象最深的或者最low的应该是dq1代,他的画面是这样的。

image

从这个游戏的画面可以明显的看到人为的瓷砖拼接痕迹,应该说瓷砖完全就是按类型划分的,中间完全没有过渡。 同样的位置,在sfc时间,已经变成了这样。

image

我们只看地形的话,特别是水与草地的连接区块,已经明显看到了有过渡,当然这种过渡本质上是美术做了第二个草地瓷砖导致的。 顺便说一句,dq1代是1986年出的,sfc版本在1993年同2代一起推出。

对于美国人来说,同样的图像进化印象最深的应该在Ultima IV到Ultima V的跨越,具体可见这个 地址

瓷砖存在的问题

使用瓷砖的首个问题恐怕跟现实中的一样,就是很明显的人造物体,而在游戏中,由于大量需要拟自然的草地,河流等,现在瓷砖首先需要处理的问题就是防止看起来像人造草地,人造河流。 其次是平滑的过渡,自然界的绝大部分地表与地表之间是有合理过渡的,比如草地与沙地,森林与草地等,而不会出现生硬的分割,比如dq1的fc版本。 最后一个问题是美术制作上的,如何生成一个可无限平铺的瓷砖,这个在技术上有一些手段,当然主要的处理还是在美术上。

瓷砖过渡

最简单的瓷砖过渡自然是人肉的方式,比如设计师在设计关卡的时候,根据周围的瓷砖选择应该选择的瓷砖,而美术在制作的时候就会制作一些过渡瓷砖,比如dq1 sfc版中主角的与水相邻的草地,就有明显的水花,这也与现实生活的观感一致,设计师在设计大地图的时候选中这块瓷砖即可。

但是关卡设计师如果每次想做个草地都要进行类似的判断,明显是一件愚蠢的行为,因为采用哪个瓷砖完全可以程序化,简单的说这个逻辑相当简单,完全可以用程序解决,而关卡设计师只要用填充工具填充一个草地即可,不需要关心用哪个过渡草地贴图。

如何处理瓷砖过渡

简单的计算下瓷砖过渡所需的块数

首先是最基本的一个类型一个瓷砖,那么块数显然是1

image

下图是我们预期达到的效果

image

看上去有点复杂,怎么计算需要多少块草瓷砖与水的过渡呢,以上图中三面环水的瓷砖举例,可以看如下的抽象图

1 0 0

0 # 0

1 1 1

#即那个瓷砖块,由这这个简易的抽象图可以明白,实际上采用那个瓷砖块由周围的8个格子决定,所以排列组合有2^8个,即256种组合,而这仅仅是草与水的组合,如果草与沙、泥土等那不得是天文数字。

所以下一步就是如何优化这个组合数量,可以看到这种组合排列方式有是以地形类型在图形中间为准的,即我们的思维方式是草,另外可以看到刚才抽象图中左上角的1对#号并没有相互影响的关系,也就是说,组合排列方式未必是相乘关系(当然想做的更好还是可以做不一样的,这两个草地相挨比较近,按道理中间应该有浅滩)。

现在来说一些详细的对策,对策一就是将4个边缘及4个对角的图片拆开处理,在程序中在动态合并起来,这样的话就只需要2^4 + 2^4 = 32个图片即可解决此问题,这种办法可以参见此文

对策二就是去除一些在游戏中不会出现或者不需要处理的过渡区块,比如刚才的草地对角(刚才也解释了,在这种情况下,实际上有会更好),对角外墙的衔接,这种处理往往在墙这种材质中比较常出现,适合泰格瑞亚这类游戏,具体可以参考此文中的Blob模式(仅需要47块),应该来说非常适合做此类游戏。

对策三是最常使用的招数,绝大部分游戏都使用此处理方式,那就是将瓷砖类型信息放在对角进行考虑,这样2^8 就直接变成了2^4即16个瓷砖组合。

image

当然这种方式也有几个问题,因为整体瓷砖有偏移,所以地表需要做偏移量,否则与人物等上层图层会有偏移。另外一块瓷砖就不仅仅是一个类型信息了,而是有四个类型信息,也就是占用内存会更多点。

在游戏应用上还有一种瓷砖过渡方式与此方式完全不同,过渡块实际上是一个单独的图片,程序会在游戏运行时动态用过渡块混合原始图片,这样美工的工作量很少很多,而且过渡块可以通用于很多资源,下图说明一切。

原理图

image

效果图 image

当然这种通用出来的过渡模板不可能通用一切类型,一般需要多个类型之间做几种,比如这个是我从帝国时代中截图得到的不同过渡块模板。

image

瓷砖类型间的过渡

刚才讨论的仅仅是两种瓷砖间的过渡,我们成功将256种可能缩减到了几十个,而如果有其他类型相互组合又如何处理呢,比如水与沙地,是否要重新制作几十个过渡块?

这个解决方法也有许多,比如在真实世界中,水一般不太可能跟砖块,铁等相互衔接,所以一般制作上不会按n*m的方式制作所有过渡块,只会在需要的时候制作即可,待会我们介绍Tiled编辑器的时候可以看到他是如何记录多个类型之间的过渡的。

另外一种处理方式比较讨巧,就是定好多个类型之间的层级关系,比如草地一定是在水上方,而岩石一定是在草地上方,而图片原型含有透明度,游戏运行是按层级做透明混合即可(war3即是这样的处理方式)。

2D游戏中的瓷砖过渡

下面我想举一些游戏中的例子,来说明下以上的做法。

编辑器Tiled

目前Tiled应该是2d瓷砖编辑器的王者,基本上包含了一个直接可用的瓷砖编辑器,另外他在0.9的版本新增了地形元素,包含了瓷砖过渡的功能。

image

可以看到沙地与鹅卵石、普通路面间均有过渡,另外操作上也很直观,采用的是方案三,即4斜角判断类型的方式,所以仅需16块瓷砖(实际上只需要15块,没有全是0的可能)即可自身瓷砖做任意平铺。不需要对角衔接的情况还可以去除两块。 具体的操作可以查看Tiled文档尝试一下。

泰格瑞亚

泰格瑞亚的采用比较极端,在Reddit上有泰格瑞亚美术的回答,他们使用的是256个过渡块,这个美术同学满是吐槽,我扒了下电脑上泰格瑞亚的游戏,还真是。

image

这个美术同学说,如果重新设计的话,他比较希望采用47块的那种方式,当然这种方式会导致对角块没有衔接,但是问题不大。

Starbound

Starbound是泰格瑞亚团队成员重新创立的一个科幻版的类泰格瑞亚,我没有仔细玩过,不过我特地研究过了一下他的tile拼接方式,发现他的格子是这样的。

image

瓷砖非常小。另外外部的衔接块只有4px,中间的块则是8px,我第一眼的直觉就是,这个应该还是类似方案三的过渡方式,上面的8×8的应该是对角衔接。于是我用ps重新做了下透明像素填充,为了能在Tiled中直接使用,于是这个泥土块变成了这样。

image

由此来看,泰格瑞亚的瓷砖过渡块是非常不人道的。顺便说下,starbound和饥荒的关卡编辑都是用Tiled制作的,虽然他们都包含了绝大部分程序化的世界生成,但是有非常多的套路关卡直接用Tiled设计了。

RPG Maker VX

这个是做RPG游戏非常有名的编辑器,出名的游戏我就不列举了,而这个编辑器给我最大的印象就是我在08年用js做一个rpg引擎时,一直苦恼于如何程序化衔接每个类型的瓷砖,而这个编辑器打开用,我很惊艳于他直接可用的过渡瓷砖,但是当时因为时间有限,并没有去深入研究他的实现,现在因为本篇文章的整理,基本上明白了他的实现原理就是我的类似方案三的实现,只不过他的地表应该是自带偏移了。他的砖块集如下,为了更明确的明白他的实现,我特地在一个可衔接砖块上标注了1到6。

image

不过我的标注应该是错误的,他实际上应该是13块的实现,即没有对角过渡。

3D游戏中的瓷砖过渡

3d游戏中普遍还是瓷砖地图,只不过绝大部分可能用了纹理混合达到细微调整。

魔兽争霸3

war3实际上在上面已经提到了,他使用15块过渡瓷砖做的地表,另外明确多个地表类型之间的层级关系,不过由于是3d游戏,有光照和阴影对场景的影响,所以导致看上去纹理并没有明显的重复。

下图是我用Tiled设定的war3草地图

image

与上面不同的是,war3有对角衔接图。

Dota2

Dota2的平面地表是可以做多材质,多纹理的混合的,而且混合效果非常好,衔接区块是不需要制作的,完全是用程序混合生成。 具体的可参考 这个 还有这个

Wang Tiles

除了边角处理以外,中间都相交的区块重复度还是很高,所以一般游戏基本上都会做多套随机使用,比如war3的图片,明显是有非常多的中间区块用来随机使用的,有没有更好的办法将这种重复感觉进一步弱化呢,答案是Wang Tiles。

image

本质上就是将随机选取中间块变成了根据更加多元化的边界及样式将人造感弱化。

文章已经有点长了,所以不太希望把这个话题再铺开,google下可以搜索得到很多相关文章,比如 这个, 这个

流亡黯道

流亡黯道的处理方式跟上文的瓷砖过渡没有太大关系,主要是使用了wang tiles的做法,做了一个比较不错的应用,我把他放在这里,官方这个教程很不错,主要是美术同学如何制作可以用来做wang tile的教程

其他方案

上文的瓷砖过渡方案主要都是美术制作的,而目前市面上3d游戏另外一部分制作是通过程序混合的,类似地形高度图的使用,应该是生成了专门的笔刷图或者顶点属性,在地图加载的时候动态混合出来使用的。 这样的话,美术可以直接在地图编辑器里作画,可以节省更多的美术资源,不过效果如何还是要看美术的功力。

比如在Dota2 Workshop中的这里文章中可以看到应该是在顶点中存了同个材质不同纹理在顶点的所占比重达到了混合效果,从dota2实际效果看,混合得挺不错的。

游戏样例分析

饥荒

image

饥荒的地表明显是Blob模式,即47块,每块为136像素 比较特别的是,饥荒的地表跟Blob模式是反向的,即实际操作中对应Blob模式的实物居然是空地,我在简单实现中发现,饥荒应该在设计阶段用的是1,0等简单的标示地表类型,而通过程序动态算出使用的区块的,因为如果是地表编辑的时候使用此方式,那么笔刷很难操作。

帝国时代

image

帝国时代的地表也是Blob模式,虽然有很多重复的上下左右贴图,不过那些应该在游戏中只是减少重复感使用的。 不过帝国时代需要衔接的地表类型比较多,所以通用了一个Blend纹理。

OpenGL freetype 创建描边效果及cocos2d-x描边问题

主要参考文章
http://www.cppblog.com/shly/archive/2013/07/14/201796.aspx
http://www.cppblog.com/wc250en007/archive/2011/07/13/150809.html
目的
想了解描边效果的主要原因是因为cocos2d-x自带的效果中(enableGlow,enableOutline,enableShadow),没有一个是满足效果的,enableGlow的效果极其难看,enableOutline的效果感觉Outline的位置总有错误,另外原始3.0版本的描边甚至都存在bug,导致字库的位置是错误的。
基本原理
cocos2d-x的标签类型有三种,位图字体,系统字体,TrueType字体
位图字体与一般的精灵图片类似,从大的精灵表中获取坐标,直接渲染,这个是最快也是最直接的方式
常用的后缀为fnt文件及对应纹理图片
系统字体系统字体为平台相关,比较少用,直接从操作系统已有的字体库中获取字体的位图信息。由于字体文件未必在特定平台都存在,因此此字体几乎没有使用
TrueType字体为矢量字体,这个也是使用频率最高的方式,但是这个效果比较低,因为在使用中,需要根据字体文件,大小,是否描边等参数生成对应的纹理信息,虽然cocos2d-x在使用后会进行缓存,但通过整体的流程可以看出比位图字体慢许多,不过好处是,在设置字体大小不同的业务需求时,字体不会因为变大而产生模糊的情况。因为原始的文件为矢量字体。
绘制描边的基本方式
网络上查询绘制描边的方式有主要几种
  1. 使用freetype的描边api,会沿着字形极其精确的绘制出满意的描边效果
  2. 八方向绘制多次原始字体
  3. 使用发光模糊效果模拟,也是使用多次绘制的方式,在底下绘制一个模糊的原始字字体,并给其填充颜色
方法2 3都会比较消耗绘制效率,2方法在描边宽度比较大的情况下,穿帮会比较严重,u3d的ngui插件使用的就是其描边方案,3方法在flash上经常使用,不过模糊算法会更加的消耗性能,因为需要大量计算周边像素值,并且有多次渲染。
问题
这里的描边主要是针对TrueType字体,TrueType字体在cocos2d-x的内部处理中使用了freetype库,但是在设置outline的处理上与ferrotype的example2存在着比较大的不同。
他的基本思路是与参考文献1类似,但是在处理原始字形的alpha值时,直接采用了绘制原始字形的bitmap值,另外与描边后的边线进行了居中对齐,最终导致了最终字形与描边位置错误比较严重,也就是比较丑。
另外描边后的偏移位置cocos2d-x也是计算错误的,需要增加两个bbox的x,y偏差值,具体可看参考文献2

自然连通图

引言

接上一文章,当每个随机区块完成后,需要对每个区块进行连接,如果更好的进行连接,随机乱连肯定是不行的,会出现如下情况

图片1

直觉上告诉我们,我们希望得到的是类似下面的图

QQ截图20140122111343

这种连接方式看上去很“自然”,那如何得到这种比较自然的连线方式呢?我们从结果出发可以得到几点规则:

  1. 连线不能交叉
  2. 连线不要经过其他点
  3. 最好不好一个点有多条连线

通过上面的约束,我们可以得到一个算法

  1. 初始时所有点都处在自己的独立集合中
  2. 从第一个集合出发,找到一个离开最近的集合
  3. 与最近的集合做连线,并将那个集合取消,将连线后的点增加到当前集合
  4. 重复第2步,直到所有点都在一个集合

代码

package
{
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.geom.Point;

	[SWF(frameRate="60", width="800", height="600")]
	public class LineMap extends Sprite
	{
		private var points:Array;
		private var lines:Vector.;
		private var units:Vector.;
		private var step:int = 0;
		private var currentPoint:Point;

		public function LineMap()
		{
			stage.addEventListener(MouseEvent.CLICK, onClick);
		}

		public function initUnit():void {
			units = new Vector.();
			// 初始时每个球一个单元
			for each (var p:Point in points) {
				var u:Unit = new Unit();
				u.points.push(p);
				units.push(u);
			}
		}

		protected function onClick(event:MouseEvent):void
		{
			init();
			initUnit();
			line();
			draw();
		}

		private function line():void
		{
			lines = new Vector.();
			// 遍历所有单元,选择出最近连线,且不相交
			while (units.length > 1) {
				var u:Unit = units[0];

				// 遍历其余单元,找出距离最短的那个单元
				var minDist:Number = Number.MAX_VALUE;
				var minIndex:int = -1, minP1:Point, minP2:Point;
				for (var j:int = 1; j < units.length; ++j) {
					var o:Unit = units[j];
					assert(o.points.length <= 1);
					var dist:Array = u.dist(o);
					if (dist[0] < minDist) {
						minDist = dist[0];
						minIndex = j;
						minP1 = dist[1];
						minP2 = dist[2];
					}
				}

				var l:Line = new Line();
				l.p1 = minP1;
				l.p2 = minP2;
				lines.push(l);

				// 将最短距离的那个单元加入当前单元
				u.points.push(units[minIndex].points[0]);
				units.splice(minIndex, 1);

//				currentPoint = minP1;

//				step++;
//				break;
			}
		}

		public function init():void {
			var count:int = 30;
			var padding:int = 10;
			points = [];
			for (var i:int = 0; i < count; ++i) {
				var p:Point = new Point(rand(padding, stage.stageWidth - padding), rand(padding, stage.stageHeight - padding));
				points.push(p);
			}
		}

		public function rand(x:Number, y:Number):Number {
			return x + Math.random() * (y - x);
		}

		public function draw():void {
			graphics.clear()
			for each (var p:Point in points) {
				graphics.beginFill(0x0);
				if (currentPoint == p) {
					graphics.beginFill(0xFF0000);
				}
				graphics.drawCircle(p.x, p.y, 10);
				graphics.endFill();
			}
			for each (var l:Line in lines) {
				graphics.lineStyle(1, 0x0);
				graphics.moveTo(l.p1.x, l.p1.y);
				graphics.lineTo(l.p2.x, l.p2.y);
			}
		}

		public function assert(b:Boolean):void {
			if (! b) {
				throw new Error("断言错误");
			}
		}
	}
}
import flash.geom.Point;

class Unit {
	public var points:Array = [];

	public function dist(u:Unit):Array {
		var minDist:Number = Number.MAX_VALUE;
		var minP1:Point, minP2:Point;
		for each (var p1:Point in points) {
			for each (var p2:Point in u.points) {
				var dist:Number = p1.subtract(p2).length;
				if (dist < minDist) {
					minDist = dist;
					minP1 = p1;
					minP2 = p2;
				}
			}
		}
		return [minDist, minP1, minP2];
	}
}

class Line {
	public var p1:Point;
	public var p2:Point;
}

效果

Get Adobe Flash player

基于着色器的线框图(wireframe)实现(stage3D)

概述

线框图的目的主要是为了调试模型情况,在OPENGL或者DX中,有专门的方法可以用来显示线框图,比如OPENGL可以用glDrawElements的GL_LINES用来绘制线框图,而在Stage3D中没有这样的方法,你能修改的,只有顶点着色器及片段着色器。

方案

基于着色器的绘制线框图方案可以参考SolidWireframe,但是在这个文章中使用的方案是几何着色器,但是我们木有这个着色器!!!

但是我们可以从这个文章中读到一些基本的实现思路,比如应该得到每个顶点与对应边的距离,然后再通过这个距离算出是否需要绘制颜色。虽然这个思路看起来比较简单,但是在实现中会遇到如下几个问题。

  • 如何得到这个距离,要知道,这个距离是在屏幕空间中的距离,而不是模型本地空间
  • 算出的线段显得锯齿比较明显,如何做平滑

问题一的解决思路就是在每个顶点属性缓冲区中增加一个属性——距离,另外,这个距离的值需要做屏幕空间变换(模型变换 * 摄像机变换 * 摄像机透视变换 + 齐次化 + 屏幕空间变换)。因为没有几何着色器,所以这个过程会显得比较费CPU,另外,因为模型顶点是公用的,如果需要实现这个功能,只能讲顶点变成重复,每个三角形的顶点变成不一样。

问题二的解决方案在参考文档中有详细说明,可以设置平滑函数I(x)

平滑2平滑3

实现代码

参考文献