<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>windam.log &#187; 程序设计</title>
	<atom:link href="http://www.windameister.org/blog/category/programming/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.windameister.org/blog</link>
	<description>learn, think, share, communication</description>
	<lastBuildDate>Fri, 04 Nov 2011 16:28:39 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.1</generator>
		<item>
		<title>Crysis 地形渲染技术剖析&#8212;&#8212;材质与LOD</title>
		<link>http://www.windameister.org/blog/2011/11/04/crysis-terrain-render-tech-anaylsis-material-and-lod/</link>
		<comments>http://www.windameister.org/blog/2011/11/04/crysis-terrain-render-tech-anaylsis-material-and-lod/#comments</comments>
		<pubDate>Thu, 03 Nov 2011 16:09:00 +0000</pubDate>
		<dc:creator>windam</dc:creator>
				<category><![CDATA[3D渲染]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Crysis]]></category>
		<category><![CDATA[terrain]]></category>
		<category><![CDATA[地形渲染]]></category>

		<guid isPermaLink="false">http://www.windameister.org/blog/?p=314</guid>
		<description><![CDATA[材质 Crysis地形的一个最重要的特色是它的地表材质系统。当进入到一定距离之后，可以看到非常精细的地表材质，包括Bump Mapping，Parallax Occlusion Mapping，有了材质系统的支持，Crysis的地形相较基于传统的Texture Splatting的地形可以呈现出令人惊叹的细节。 在POM技术支撑下的Crysis地形： 基于Texture Splatting技术的地形： Crysis的地表材质技术的原理如下： 渲染包括两个步骤，首先基于颜色图和法线图，绘制基本的地形色彩与明暗，这一遍渲染结果主要用于远处地形。然后会针对不同材质，将细节通过AlphaBlend融合到之前的渲染结果上，材质细节通常只在离视野较近的区域内才渲染，因此第二个步骤的耗费并不多，根据实际情况，每帧中大约会有从一百到数百个不等的细节层Mesh被绘制。 第一遍基于颜色图渲染的效果如下： 可以看到，颜色图上提供了明暗细节，这对地形渲染中无处不在的Tiling问题起到了缓解作用。 第二遍Blend上细节之后的效果： Crysis为每个顶点赋予一个材质id，并且根据不同的材质id，将原先完整的地形网格划分成按材质组织的小块网格，（如果整块32×32的网格都是同一种材质，则无需划分），小块网格是通过索引区段的方式定义的。在Crysis中，地形顶点数据结构里Color属性的g分量被用了作材质层的ID。 下图展示了一个网格中的33×33个顶点被区分为两种材质的情况。本图为演示，将顶点颜色涂成不同的颜色，Crysis则是将将材质id通过顶点颜色传入vs。下面的网格中存在两种不同的材质需要分别渲染。 Crysis根据顶点材质id将整块Mesh拆分成以材质id分组的小块Mesh，如图： 不同材质的Mesh之间，有一个三角形的过渡带。 上图中，红色箭头表示草的材质从1过渡到0，蓝色箭头表示沙子材质从1过渡到0。 这个过渡带起到的作用正是让一个处于材质交汇带的三角形，其Alpha可以从1过渡到0。这样和相邻的隶属另一个材质正好融合到一起。 因此，材质过渡区域的三角形会被渲染2到3遍，取决于该三角形的三个顶点分属于2个材质还是3个材质。 上述的融合过程是通过下面这段代码做到的（本文只为解释原理，与真实代码有出入，Crysis的Shader代码没有加密，有心人可自己解包查阅）： 声明常量： float g_InputLayerId; 该常量通过程序传入VS常量中，表示当前渲染Mesh的LayerId。 在vs中计算顶点alpha： float alpha = 1- saturate (g_InputLayerId – Input.Color.g * 255.0f); Input.Color.g中存储的是该顶点所属的LayerId，由于是通过COLOR存储，取值范围是从0.0到1.0，因此在shader里用于计算之前，还需要乘以255.0f还原。 上式的逻辑如下： 如果InputLayerId和当前顶点存储的layerId相同，则alpha为1，否则，如g_InputLayerId与当前顶点上的layerId不同，则alpha为0。 因为可以使用不同材质做细节层的渲染，Bump Mapping，Parallax Occlusion Mapping这类技术对于地表细节的表现提升极为明显。除了Bump Map之外，Crysis还将Detail Bump Map，Specular Map等用于提升材质细节的技术纳入到地形细节层的渲染中。 LOD Crysis为了支撑8公里乘8公里的超大视距（由于Crysis中的网格Size是2米一格，一块地形拥有多达4096*4096个网格），在最高精度下，总共三千二百万个三角形。因此必须有一套合理的地形LOD方案。 Crysis的地形LOD，从地形LOD算法的分类上看，是基于Geo-Mipmapping的。不过相比最传统的Geo-Mipmapping，Crysis利用四叉树的特性，并且考虑到最新图形硬件的特点，针对地形网格做了相当多的优化。 首先解释一下Geo-Mipmapping的基本思想： Geo-Mipmapping始于Willem H. de [...]]]></description>
			<content:encoded><![CDATA[<h4>材质</h4>
<p>Crysis地形的一个最重要的特色是它的地表材质系统。当进入到一定距离之后，可以看到非常精细的地表材质，包括Bump Mapping，Parallax Occlusion Mapping，有了材质系统的支持，Crysis的地形相较基于传统的Texture Splatting的地形可以呈现出令人惊叹的细节。</p>
<p>在POM技术支撑下的Crysis地形：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image002.jpg"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image002" src="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image002_thumb.jpg" alt="clip_image002" width="651" height="322" border="0" /></a></p>
<p>基于Texture Splatting技术的地形：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image004.jpg"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image004" src="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image004_thumb.jpg" alt="clip_image004" width="650" height="416" border="0" /></a></p>
<p>Crysis的地表材质技术的原理如下：</p>
<p>渲染包括两个步骤，首先基于颜色图和法线图，绘制基本的地形色彩与明暗，这一遍渲染结果主要用于远处地形。然后会针对不同材质，将细节通过AlphaBlend融合到之前的渲染结果上，材质细节通常只在离视野较近的区域内才渲染，因此第二个步骤的耗费并不多，根据实际情况，每帧中大约会有从一百到数百个不等的细节层Mesh被绘制。<span id="more-314"></span></p>
<p>第一遍基于颜色图渲染的效果如下：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image006.jpg"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image006" src="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image006_thumb.jpg" alt="clip_image006" width="647" height="342" border="0" /></a></p>
<p>可以看到，颜色图上提供了明暗细节，这对地形渲染中无处不在的Tiling问题起到了缓解作用。</p>
<p>第二遍Blend上细节之后的效果：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image008.jpg"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image008" src="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image008_thumb.jpg" alt="clip_image008" width="651" height="336" border="0" /></a></p>
<p>Crysis为每个顶点赋予一个材质id，并且根据不同的材质id，将原先完整的地形网格划分成按材质组织的小块网格，（如果整块32×32的网格都是同一种材质，则无需划分），小块网格是通过索引区段的方式定义的。在Crysis中，地形顶点数据结构里Color属性的g分量被用了作材质层的ID。</p>
<p>下图展示了一个网格中的33×33个顶点被区分为两种材质的情况。本图为演示，将顶点颜色涂成不同的颜色，Crysis则是将将材质id通过顶点颜色传入vs。下面的网格中存在两种不同的材质需要分别渲染。</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image010.jpg"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image010" src="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image010_thumb.jpg" alt="clip_image010" width="507" height="398" border="0" /></a></p>
<p>Crysis根据顶点材质id将整块Mesh拆分成以材质id分组的小块Mesh，如图：</p>
<p>不同材质的Mesh之间，有一个三角形的过渡带。</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image012.jpg"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image012" src="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image012_thumb.jpg" alt="clip_image012" width="643" height="361" border="0" /></a></p>
<p>上图中，红色箭头表示草的材质从1过渡到0，蓝色箭头表示沙子材质从1过渡到0。</p>
<p>这个过渡带起到的作用正是让一个处于材质交汇带的三角形，其Alpha可以从1过渡到0。这样和相邻的隶属另一个材质正好融合到一起。</p>
<p>因此，材质过渡区域的三角形会被渲染2到3遍，取决于该三角形的三个顶点分属于2个材质还是3个材质。</p>
<p>上述的融合过程是通过下面这段代码做到的（本文只为解释原理，与真实代码有出入，Crysis的Shader代码没有加密，有心人可自己解包查阅）：</p>
<p>声明常量：</p>
<p>float g_InputLayerId;</p>
<p>该常量通过程序传入VS常量中，表示当前渲染Mesh的LayerId。</p>
<p>在vs中计算顶点alpha：</p>
<p>float alpha = 1- saturate (g_InputLayerId – Input.Color.g * 255.0f);</p>
<p>Input.Color.g中存储的是该顶点所属的LayerId，由于是通过COLOR存储，取值范围是从0.0到1.0，因此在shader里用于计算之前，还需要乘以255.0f还原。</p>
<p>上式的逻辑如下：</p>
<p>如果InputLayerId和当前顶点存储的layerId相同，则alpha为1，否则，如g_InputLayerId与当前顶点上的layerId不同，则alpha为0。</p>
<p>因为可以使用不同材质做细节层的渲染，Bump Mapping，Parallax Occlusion Mapping这类技术对于地表细节的表现提升极为明显。除了Bump Map之外，Crysis还将Detail Bump Map，Specular Map等用于提升材质细节的技术纳入到地形细节层的渲染中。</p>
<h4>LOD</h4>
<p>Crysis为了支撑8公里乘8公里的超大视距（由于Crysis中的网格Size是2米一格，一块地形拥有多达4096*4096个网格），在最高精度下，总共三千二百万个三角形。因此必须有一套合理的地形LOD方案。</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image014.jpg"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image014" src="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image014_thumb.jpg" alt="clip_image014" width="651" height="416" border="0" /></a></p>
<p>Crysis的地形LOD，从地形LOD算法的分类上看，是基于<a href="http://www.flipcode.com/archives/Fast_Terrain_Rendering_Using_Geometrical_MipMapping.shtml">Geo-Mipmapping</a>的。不过相比最传统的Geo-Mipmapping，Crysis利用四叉树的特性，并且考虑到最新图形硬件的特点，针对地形网格做了相当多的优化。</p>
<p>首先解释一下Geo-Mipmapping的基本思想：</p>
<p>Geo-Mipmapping始于<a href="mailto:whdeboer@iname.com">Willem H. de Boer</a>的论文<a href="http://www.flipcode.com/tutorials/tut_geomipmaps.shtml">Fast Terrain Rendering Using Geometrical MipMapping</a> (2000)。</p>
<p>与近处的地形相比，离摄像机远的Block不必采用同样的细节的网格，我们可以采用一个更低精度的版本来近似。通过减少三角形的数量，获得渲染效率上的提升。</p>
<p>考虑传统意义上的纹理MipMapping技术。对每张纹理，都会创建一条mipmap链，链中的第一级贴图就是实际的纹理，而后的每一级都是将上一级的分辨率缩小一半得到的。当贴图距摄像机一定距离的时候，mipmap链中的合适的层级会被选择出来用于渲染，而不是选择最高分辨率的纹理。我们也可以将同样的原理应用到三维网格中，在地形中，等价于纹理的就是地形Block，地形Block的mipmap链则由最高分辨率版本的Block通过缩小分辨率获得。</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image016.jpg"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image016" src="http://www.windameister.org/blog/wp-content/uploads/2011/11/clip_image016_thumb.jpg" alt="clip_image016" width="383" height="356" border="0" /></a></p>
<p>如上图：（图来自于<a href="http://www.flipcode.com/archives/article_geomipmaps.pdf">这里</a>）</p>
<p>白色的点位MipMap Level 0，黑点为MipMap Level 1，依次类推。</p>
<p>传统的GeoMipMapping技术就是采用上述方案，例如对一个32×32格子的原始网格依次创建16×16，8×8格子的MipMap链。随着地形Block到摄像机距离的增加，逐渐采用越来越稀疏的版本。</p>
<p>Crysis的地形LOD的基础就是基于上述思想——其实这是废话，基本上大多数的地形LOD算法都是基于这个思想。</p>
<p>所不同的是，Crysis利用四叉树结构对GeoMipMapping做了优化。</p>
<p>Crysis渲染的基本单位是32×32的网格。也就是说，一次draw call最多向图形管线传递32×32×2个三角形。</p>
<p><strong>随着距离增加，32*32</strong><strong>的网格会降级成16*16</strong><strong>，如果四个紧挨着的Block</strong><strong>都变成16</strong><strong>×16</strong><strong>，且这四个Block</strong><strong>都是同一个父节点的子，则会合并成一个LOD</strong><strong>级别更高的32</strong><strong>×32</strong><strong>的网格。</strong></p>
<p>换一个角度看，把地形Mesh看成一棵四叉树，处于叶节点的Block拥有最高精度网格，每向上一层，精度都会缩减一半（总的分辨率变成之前的一半）。</p>
<p>这样假设叶节点层（第0层）的Mesh是一个1025*1025个网格，那么第1层就是513*513的网格，第2层257*257，第3层129*129，第4层65*65，第5层33*33。</p>
<p>如果我们选择第5层进行渲染，只需要一次Draw Call就可以将整个Mesh绘制完毕，当然在精度上也会有很大损失。因此，可以这样考虑，从根节点开始遍历，如果当前节点距离摄像机的距离很近，以至于该节点自身的细节不足够精确，并且该节点还有子节点，则我们将该节点的四个子节点而非该节点本身置入渲染队列，否则意味着该节点的细节就足以应付需求，因此可以只渲染该节点。</p>
<p>上述方式由于为整个Mesh创建了一个MipMap链，因此会增加33%的冗余顶点数据，但是利用这个原理，Crysis成功将4096×4096的地形DrawCall次数降低到了300～500次。这便是Crysis能够高效渲染超大视距地形的原理。</p>
<p>&nbsp;</p>
<p>最后谈谈针对GPU的优化：</p>
<p>现代GPU的特点是有大量的流处理器，支持高并发的运算量，顶点计算也好，像素计算也好，都是可以高度并行化的，现代GPU针对这些做了大量的工作。</p>
<p>GPU的显存越做越大，1GB的显存已经是小儿科，动辄2GB，甚至更多的显存也能在市场上见到。</p>
<p>也就是说，随着GPU的发展，运算和存储都逐渐不再是瓶颈——那么相比之下，通过PCIE总线把数据从CPU传输到显卡则成了一个制约性能的瓶颈所在了。</p>
<p>以往我们习惯于每帧Lock一个Buffer，然后通过CPU动态填充数据到Buffer里，再把这个Buffer提交到Device上去用于渲染。这种做法就会导致每帧都要传递大量的数据到显卡上去。相比之下，现代GPU的结构更倾向于让我们进行这样的渲染：在初始化阶段就将数据创建成静态的Buffer并存放于显存里，每帧渲染都利用这些现有的Buffer。</p>
<p>针对地形来说，顶点数据很好说，因为本来就不会在运行期进行修改，因此可以直接创建成静态的Buffer，而索引数据往往会引发疑惑，因为LOD时时刻刻都在变化。地形LOD因为每帧要动态更新Block的LOD状态（包括自身的LOD级别以及和相邻Block之间的接缝），因此不得不去每帧Lock索引的Buffer，动态填充。</p>
<p>但如果仔细想想，就会发现索引也是可以预计算的。（Crysis就采用了预算好的索引Buffer）</p>
<p>考虑一个32×32的网格，它的索引大小为32*32*2(三角形) * 3(索引)* 2(sizeof(WORD))= 12KB</p>
<p>考虑周围邻接Block的LOD级别高于它的情况，每条邻接边都可能是[16, 8, 4, 2, 1]个Grid，也就是有5种情况，一共有4个邻接边。这样总的数据量是：5* 5* 5 * 5 * 12KB 约等于 7.32 MB。这是我们允许跨5级LOD的极端情况，在Crysis中我没有观察到跨5级LOD的情况，而如果我们只允许跨3级LOD，也就是对于32*32的Block的邻接Block只允许[16, 8, 4]三种情况，则总的数据量约为 3 * 3 * 3 * 3 * 12KB = 972KB。</p>
<p>如果每个Block内部允许存在3级LOD，也就是[32*32, 16*16, 8*8]三种，其中16* 16的网格，其索引数据占用空间为32*32的1/4，即3KB，8*8网格的索引数据则不到1KB。</p>
<p>也就是说，即便允许Block内部3级LOD，同时允许每个Block与邻接Block跨3级LOD级别，总的索引数据量也不过在1.5MB左右。完全可以预计算好然后存在显存里。</p>
<p>在渲染当中，当Block的LOD级别被确定下来的时候，根据Block的内部LOD级别以及该Block与邻接Block的LOD差作为索引，就可以查找到需要的IndexBuffer了。</p>
<p>===分割线===</p>
<p>由于没有CryEngine的代码，分析所依赖的手段只有PIX抓帧和PerfHud，以及CryEngine的Shader，所以本文中所述的观点仅供参考，欢迎讨论。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.windameister.org/blog/2011/11/04/crysis-terrain-render-tech-anaylsis-material-and-lod/feed/</wfw:commentRss>
		<slash:comments>10</slash:comments>
		</item>
		<item>
		<title>关于地形渲染技术的杂谈</title>
		<link>http://www.windameister.org/blog/2011/08/28/some-tech-topics-on-terrain-rendering/</link>
		<comments>http://www.windameister.org/blog/2011/08/28/some-tech-topics-on-terrain-rendering/#comments</comments>
		<pubDate>Sun, 28 Aug 2011 14:28:31 +0000</pubDate>
		<dc:creator>windam</dc:creator>
				<category><![CDATA[3D渲染]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[LOD]]></category>
		<category><![CDATA[Virtual Texture]]></category>
		<category><![CDATA[地形渲染]]></category>
		<category><![CDATA[材质]]></category>
		<category><![CDATA[阴影]]></category>

		<guid isPermaLink="false">http://www.windameister.org/blog/2011/08/28/%e5%85%b3%e4%ba%8e%e5%9c%b0%e5%bd%a2%e6%b8%b2%e6%9f%93%e6%8a%80%e6%9c%af%e7%9a%84%e6%9d%82%e8%b0%88/</guid>
		<description><![CDATA[地形是3D游戏中不可或缺的基础部分，特别是对于要表现室外场景的游戏来说。随着图形硬件的发展，地形渲染技术也经历了若干变迁。主要包括几个方面：LOD，光照和阴影，材质表现。由于本人的时间精力有限不可能面面俱到，所以只能是浅谈辄止，粗浅的叙述一下相关的技术和一些思考，欢迎交流指正。 关于LOD 早些年，由于图形硬件的瓶颈在于顶点和三角形处理上，因此发展出一批目的在于尽可能减少顶点和三角形数量的算法，如ROAM。后来随着显卡性能的提升，渲染效率的瓶颈逐渐变成了总线上的数据传输速率，地形渲染随之演变成尽可能将预计算好的数据存储于显卡上，从而避免每帧传递数据，如Geo-Mipmapping。还有后续的算法在此基础上继续改进，如Chunked LOD。 除此之外，地形LOD算法还需要处理的问题包括如何消除LOD级别改变时发生的视觉上的突变。一种常见的方法就是通过Morphing。不过不知是出于效率还是工程上的考虑，Crysis的地形上并没有做morphing。 再后来，随着Shader Model 3.0中增加了Vertex Texture访问以及对Instancing的支持，地形渲染技术则有了更多的选择，例如通过VertexTexture技术，可以在VertexShader中算顶点，而Instancing技术更进一步减少了数据传输量，进一步简化了CPU端的计算和数据传输。 参考：CDLOD，这个作者正在基于DX11实现他的下一个版本的CDLOD，将会包含下面要说的Tessellation技术。 到了DX11，新引入的Tessellation技术则让引擎开发者拥有了将整个地形渲染完全搬到显卡上去做的能力。如果说SM3.0中我们只是在硬件上动态生成所需的地形顶点，那么到了DX11中，我们就可以在硬件上实现LOD了，我们可以GPU上决定每个Patch应该有多细密的网格，并且在GPU上处理好和邻接Patch的接缝问题。关于这个话题可以参考GPU Pro2中的Terrain And Ocean Rending with Hardware Tessellation。 这意味着到DX11普及的时候，以往的那些在用于CPU端计算LOD索引的很多算法，都不再被需要了。当然，上述算法中产生的核心思想仍然会继续起作用——例如利用四叉树做地形的数据管理，例如如何在LOD改变时避免视觉上看到跳变等等。 p.s.有时候难免对于国内的Win7普及率感到无奈…… 关于光照和阴影 由于地形是一种静态的模型，不会移动，相对光源的位置是不变的，因此实时渲染地形的时候，很流行的方法是采用LightMap对地形光影做预计算，而传统意义上的光照计算，则往往是在vs阶段（这里指顶点光照，若是逐像素光照，则是在ps阶段）实时进行。 由于LightMap省却了实时进行光照计算的计算量，在效率上有所提升，并且一些对计算量要求较高的技术如全局光照，如果采用实时计算会导致性能急剧下降，因此在实际中往往也是预计算并存储于LightMap中。 另一方面，LightMap不便存储动态的光照信息，当光源的角度，颜色发生改变，LightMap就需要随之更新。而这在实时游戏中往往很难做到。 目前主流的实时阴影技术基本都是基于ShadowMap的，在Doom3中使用的Shadow Volume，现在已经很少看到了。 ShadowMap的基本原理是从光源的位置和朝向绘制一张场景的深度图，成为ShadowMap，然后再在实际渲染场景的时候，将每个像素的位置到光源的距离和ShadowMap中的深度值进行比较，如果发现该像素的深度小于ShadowMap中对应位置像素的深度，则不在阴影中，反之，则在阴影中。 ShadowMap的好处在于可以方便的实现很多个物体的阴影，物体的自阴影。 但是也有几个问题： １. 边缘会出现难看的锯齿状走样，一方面因为像素的含义是深度，因此相邻像素的值是跳变的，可能一个值为7，邻接的就是200，你无法对深度做过滤；另一方面和ShadowMap的大小有关，同一个场景，使用的ShadowMap的Size越小，越会出现较大的锯齿。 ２. 如上所述，由于存储的是深度，因此ShadowMap难以判断一个像素是否处在阴影边缘中，难以给出柔和的半影过度 ３. ShadowMap只适合于平行光和SpotLight，对于点光源需要计算多达６张ShadowMap（利用DPSM可以减少到2张） 为了解决ShadowMap局限，有很多方法被发明出来，例如，采用PCF获取软边缘，不过PCF需要多次自行采样纹理，效率上有损失，另一方面PCF不是真的计算软阴影，而是通过采样的手段去找出边缘，所以无法根据距离远近产生真实的半影过度。 还有种方法是VSM（方差阴影贴图），其原理是通过车比雪夫不等式(Chebyshev)去计算一个像素在阴影中的概率： 公式来自于Variance Shadow Maps by William Donnelly_ Andrew Lauritzen† 具体来说，它在渲染深度的时候同时存储深度和深度的平方（不同的颜色通道中），然后对ShadowMap做过滤，这样得到的就逐像素的深度的期望μ和深度平方的期望，利用这两个值可以求得方差σ，然后再用车比雪夫不等式计算概率。（具体原理及实现可参考 vsm） 关于材质表现 传统的地形表现比较常见的是用多层纹理混合，也称为Texture splatting。 （图片来自Wikipedia） 这种做法可以实现多种贴图之间的混合过度。优点是实现简单，而且贴图通过反复Tiling即可构成整个地形表面，从而节省了纹理资源。 缺点则是，如果美术稍微偷点懒，就可以看到大片大片的Tiling，重复感非常明显。以魔兽世界为代表的一大批游戏引擎用的都是Texture splatting来表现地表纹理。 在Crysis中有一个在表现效果上和效率上结合得非常完美的材质系统，Crysis为了给地表提供尽可能丰富的细节，支持给顶点赋材质，这样就能够支持地表的Bump [...]]]></description>
			<content:encoded><![CDATA[<p>地形是3D游戏中不可或缺的基础部分，特别是对于要表现室外场景的游戏来说。随着图形硬件的发展，地形渲染技术也经历了若干变迁。主要包括几个方面：LOD，光照和阴影，材质表现。由于本人的时间精力有限不可能面面俱到，所以只能是浅谈辄止，粗浅的叙述一下相关的技术和一些思考，欢迎交流指正。</p>
<h4>关于LOD</h4>
<p>早些年，由于图形硬件的瓶颈在于顶点和三角形处理上，因此发展出一批目的在于尽可能减少顶点和三角形数量的算法，如<a href="https://graphics.llnl.gov/ROAM/">ROAM</a>。后来随着显卡性能的提升，渲染效率的瓶颈逐渐变成了总线上的数据传输速率，地形渲染随之演变成尽可能将预计算好的数据存储于显卡上，从而避免每帧传递数据，如<a href="http://www.flipcode.com/archives/Fast_Terrain_Rendering_Using_Geometrical_MipMapping.shtml">Geo-Mipmapping</a>。还有后续的算法在此基础上继续改进，如<a href="http://tulrich.com/geekstuff/chunklod.html">Chunked LOD</a>。</p>
<p>除此之外，地形LOD算法还需要处理的问题包括如何消除LOD级别改变时发生的视觉上的突变。一种常见的方法就是通过<a href="http://www.google.com.hk/search?hl=zh-CN&amp;newwindow=1&amp;safe=strict&amp;biw=1920&amp;bih=932&amp;q=terrain+Morphing&amp;oq=terrain+Morphing&amp;aq=f&amp;aqi=&amp;aql=&amp;gs_sm=e&amp;gs_upl=61681l63742l0l63882l11l7l0l0l0l0l0l0ll0l0">Morphing</a>。不过不知是出于效率还是工程上的考虑，Crysis的地形上并没有做morphing。</p>
<p>再后来，随着Shader Model 3.0中增加了<a href="http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter18.html">Vertex Texture</a>访问以及对Instancing的支持，地形渲染技术则有了更多的选择，例如通过VertexTexture技术，可以在VertexShader中算顶点，而Instancing技术更进一步减少了数据传输量，进一步简化了CPU端的计算和数据传输。</p>
<p>参考：<a href="http://vertexasylum.com/2010/07/11/oh-no-another-terrain-rendering-paper/">CDLOD</a>，这个作者正在基于DX11实现他的下一个版本的CDLOD，将会包含下面要说的Tessellation技术。</p>
<p>到了DX11，新引入的Tessellation技术则让引擎开发者拥有了将整个地形渲染完全搬到显卡上去做的能力。如果说SM3.0中我们只是在硬件上动态生成所需的地形顶点，那么到了DX11中，我们就可以在硬件上实现LOD了，我们可以GPU上决定每个Patch应该有多细密的网格，并且在GPU上处理好和邻接Patch的接缝问题。关于这个话题可以参考<a href="http://book.douban.com/subject/6064395/">GPU Pro2</a>中的Terrain And Ocean Rending with Hardware Tessellation。</p>
<p>这意味着到DX11普及的时候，以往的那些在用于CPU端计算LOD索引的很多算法，都不再被需要了。当然，上述算法中产生的核心思想仍然会继续起作用——例如利用四叉树做地形的数据管理，例如如何在LOD改变时避免视觉上看到跳变等等。<span id="more-289"></span></p>
<p>p.s.有时候难免对于国内的Win7普及率感到无奈……</p>
<h4>关于光照和阴影</h4>
<p>由于地形是一种静态的模型，不会移动，相对光源的位置是不变的，因此实时渲染地形的时候，很流行的方法是采用LightMap对地形光影做预计算，而传统意义上的光照计算，则往往是在vs阶段（这里指顶点光照，若是逐像素光照，则是在ps阶段）实时进行。</p>
<p>由于LightMap省却了实时进行光照计算的计算量，在效率上有所提升，并且一些对计算量要求较高的技术如全局光照，如果采用实时计算会导致性能急剧下降，因此在实际中往往也是预计算并存储于LightMap中。</p>
<p>另一方面，LightMap不便存储动态的光照信息，当光源的角度，颜色发生改变，LightMap就需要随之更新。而这在实时游戏中往往很难做到。</p>
<p>目前主流的实时阴影技术基本都是基于ShadowMap的，在Doom3中使用的<a href="http://en.wikipedia.org/wiki/Shadow_volume">Shadow Volume</a>，现在已经很少看到了。</p>
<p>ShadowMap的基本原理是从光源的位置和朝向绘制一张场景的深度图，成为ShadowMap，然后再在实际渲染场景的时候，将每个像素的位置到光源的距离和ShadowMap中的深度值进行比较，如果发现该像素的深度小于ShadowMap中对应位置像素的深度，则不在阴影中，反之，则在阴影中。</p>
<p>ShadowMap的好处在于可以方便的实现很多个物体的阴影，物体的自阴影。</p>
<p>但是也有几个问题：</p>
<p>１. 边缘会出现难看的锯齿状走样，一方面因为像素的含义是深度，因此相邻像素的值是跳变的，可能一个值为7，邻接的就是200，你无法对深度做过滤；另一方面和ShadowMap的大小有关，同一个场景，使用的ShadowMap的Size越小，越会出现较大的锯齿。</p>
<p>２. 如上所述，由于存储的是深度，因此ShadowMap难以判断一个像素是否处在阴影边缘中，难以给出柔和的半影过度</p>
<p>３. ShadowMap只适合于平行光和SpotLight，对于点光源需要计算多达６张ShadowMap（利用<a href="http://www.google.com.hk/search?hl=zh-CN&amp;newwindow=1&amp;safe=strict&amp;biw=1920&amp;bih=896&amp;q=DualParaboloidMappingInTheVertexShader&amp;oq=DualParaboloidMappingInTheVertexShader&amp;aq=f&amp;aqi=&amp;aql=&amp;gs_sm=s&amp;gs_upl=34108l34108l0l34862l1l1l0l0l0l0l0l0ll0l0">DPSM</a>可以减少到2张）</p>
<p>为了解决ShadowMap局限，有很多方法被发明出来，例如，采用PCF获取软边缘，不过PCF需要多次自行采样纹理，效率上有损失，另一方面PCF不是真的计算软阴影，而是通过采样的手段去找出边缘，所以无法根据距离远近产生真实的半影过度。</p>
<p>还有种方法是VSM（方差阴影贴图），其原理是通过车比雪夫不等式(Chebyshev)去计算一个像素在阴影中的概率：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/08/clip_image002.jpg"><img style="background-image: none; margin: 0px; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image002" src="http://www.windameister.org/blog/wp-content/uploads/2011/08/clip_image002_thumb.jpg" alt="clip_image002" width="244" height="64" border="0" /></a></p>
<p>公式来自于Variance Shadow Maps by William Donnelly_ Andrew Lauritzen†</p>
<p>具体来说，它在渲染深度的时候同时存储深度和深度的平方（不同的颜色通道中），然后对ShadowMap做过滤，这样得到的就逐像素的深度的期望μ和深度平方的期望，利用这两个值可以求得方差σ，然后再用车比雪夫不等式计算概率。（具体原理及实现可参考 <a href="http://www.punkuser.net/vsm/">vsm</a>）</p>
<h4>关于材质表现</h4>
<p>传统的地形表现比较常见的是用多层纹理混合，也称为<a href="http://en.wikipedia.org/wiki/Texture_splatting">Texture splatting</a>。</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/08/clip_image004.gif"><img style="background-image: none; margin: 0px; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image004" src="http://www.windameister.org/blog/wp-content/uploads/2011/08/clip_image004_thumb.gif" alt="clip_image004" width="244" height="191" border="0" /></a> （图片来自Wikipedia）</p>
<p>这种做法可以实现多种贴图之间的混合过度。优点是实现简单，而且贴图通过反复Tiling即可构成整个地形表面，从而节省了纹理资源。</p>
<p>缺点则是，如果美术稍微偷点懒，就可以看到大片大片的Tiling，重复感非常明显。以魔兽世界为代表的一大批游戏引擎用的都是Texture splatting来表现地表纹理。</p>
<p>在Crysis中有一个在表现效果上和效率上结合得非常完美的材质系统，Crysis为了给地表提供尽可能丰富的细节，支持给顶点赋材质，这样就能够支持地表的<a href="http://en.wikipedia.org/wiki/Bump_mapping">Bump Mapping</a>以及更高级的<a href="http://en.wikipedia.org/wiki/Parallax_occlusion_mapping">POM</a>。而传统的Texture splatting在混合时不去区分实际的材质，是不可能做到这一点的。当离远之后，Crysis就不再渲染细节纹理的pass，而是只渲染地表颜色图。</p>
<p>来一张Crysis的炫技图（图片来自互联网）：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/08/clip_image006.jpg"><img style="background-image: none; margin: 0px; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image006" src="http://www.windameister.org/blog/wp-content/uploads/2011/08/clip_image006_thumb.jpg" alt="clip_image006" width="244" height="153" border="0" /></a></p>
<p>接下来，为了引出Virtual Texturing，先介绍下Clipmap。</p>
<p>在类似GoogleEarth之类的地图软件中可能使用了一种类似于<a href="http://www.cs.virginia.edu/~gfx/Courses/2002/BigData/papers/Texturing/Clipmap.pdf">Clipmap</a>的技术，这种技术基于mipmap，同时又对之作了扩展。</p>
<p>传统的Mipmap是从一张贴图原始的Size开始直到Size为1有一个完整的Mipmap链，比如一张1024×1024的贴图，会有从512×512直到1×1的全部的mipmap版本，同时存在于显卡中，到了纹理采样的时候根据mip级别采样不同的纹理。</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/08/clip_image008.jpg"><img style="background-image: none; margin: 0px; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image008" src="http://www.windameister.org/blog/wp-content/uploads/2011/08/clip_image008_thumb.jpg" alt="clip_image008" width="244" height="116" border="0" /></a></p>
<p>图片来自<a href="http://www.cs.virginia.edu/~gfx/Courses/2002/BigData/papers/Texturing/Clipmap.pdf">The Clipmap: A Virtual Mipmap</a></p>
<p>而Clipmap中的贴图可能很大，比如64000×64000，这样就不可能把它和它的完整的mipmap链放到显卡里去了，所以他只放最低级别的那些Mipmap到显卡里，超出显卡能力的部分也放，但是只放一部分（需要的），就好象是Clip过了一样，所以叫Clipmap （= =）如图：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2011/08/clip_image010.jpg"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="clip_image010" src="http://www.windameister.org/blog/wp-content/uploads/2011/08/clip_image010_thumb.jpg" alt="clip_image010" width="244" height="92" border="0" /></a></p>
<p>虽然我们无法把整个贴图完全加载到显卡上，但是我们观察到足够清晰细节的距离不可能太远，离远了就切到下一个mip级别，所以随着到视点距离的增加，从最高精度到最低精度有一个逐渐的变化过程。所以这种做法是可行的。</p>
<p>很多人把Clipmap和Virtual Texture搞混了。其实这是两个完全不同的技术。虽然在对Mipmap的使用方面有类似的地方。</p>
<p>我觉得未来的趋势应该是把地形和模型的贴图都统一纳入到<a href="http://en.wikipedia.org/wiki/MegaTexture">Virtual Texture</a>中管理，这个技术是由卡马克在id tech 5中引入的。在Id Tech5中，通过Virtual Texture技术可以制作没有任何重复感的地表（Virtual Texture也支持模型贴图），从此再也不愁Tiling的问题——只不过可能就得苦了美术和编辑器制作人员了，而且Virtual Texture技术存在许多在制作流程上需要克服的问题。</p>
<p>Virtual Texture说白了就是一张巨大无比的大纹理，可能有1024000 * 1024000这么大，存储在磁盘上。但是到游戏中，只加载实际需要的就可以——实际需要一方面是指mip level上的需要，远处的只需要低精度的纹理，近处才需要高精度，另一方面是指不需要加载那些不进入可视范围内的纹理（看不到的）。</p>
<p>Virtual Texture技术实际上借鉴了计算机中的Virtual Memory的理念。我们知道早期的计算机物理内存没有4GB，但是应用程序却可以通过32位指针访问到整个4GB的地址空间，好像这么大的内存都可以访问一样，但是实际的物理内存可能很小，只有512MB，这是怎么做到的呢？</p>
<p>操作系统把内存按页进行管理，每个页4KB大，并且只把应用程序需要访问的内存页放到到实际的物理内存中，如果应用程序访问一块内存，而这块内存又不在物理内存中存在，那么应用程序会暂停执行，操作系统执行换入操作，并把应用程序需要的内存页拿到物理内存里，然后再把执行权交回给应用程序，当然这一切对于应用程序的开发者来说都是透明的。</p>
<p>虚拟纹理（<a href="http://en.wikipedia.org/wiki/MegaTexture">Virtual Texture</a>）也是采用类似的方案，实际的Physical Texture（想像成对应于物理内存的实际纹理）可能只有4096 * 4096这么大（根据实际显卡的支持程度决定），但是3D游戏中各物体的uv坐标则可以寻址远超出这个物理纹理范围的大纹理，因此virtual texture也需要把物理纹理分页，比如切成256×256的小块纹理，并且根据实际需要进行换入换出操作。这里一共存在从硬盘到内存，再从内存到显卡的三级缓存，这个过程中有许多复杂的地方要处理，比如怎么知道应该加载什么mip级别的贴图，怎么知道应该加载那块贴图等等。基本的思路是通过预先渲染一个pass，将需要加载的mip级别和具体的页面索引算出来。</p>
<p>Virtual Texture目前网上已经有了开源的实现，感兴趣的朋友可以参考下 <a href="http://silverspaceship.com/src/svt/">Sparse Virtual Texture</a>，<a href="http://forum.beyond3d.com/showthread.php?t=55594">这里</a>，还有<a href="http://s09.idav.ucdavis.edu/talks/05-JP_id_Tech_5_Challenges.pdf">id Tech5的介绍</a>，<a href="http://crytek.com/cryengine/presentations/advanced-virtual-texture-topics">CryEngine对Virtual Texture的看法</a>。</p>
<p>本文中提到的技术，在实际的实现中都要解决各种实现上或者工程上的问题，不可能如我在这里夸夸其谈这样简单，所以本文只能是简述下基本思想，算是对近期一段时间的学习做个整理。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.windameister.org/blog/2011/08/28/some-tech-topics-on-terrain-rendering/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>自动构建二：CruiseControl.NET配置基础</title>
		<link>http://www.windameister.org/blog/2011/06/12/autobuild_basics_on_cruisecontrol_net_cofiguration/</link>
		<comments>http://www.windameister.org/blog/2011/06/12/autobuild_basics_on_cruisecontrol_net_cofiguration/#comments</comments>
		<pubDate>Sun, 12 Jun 2011 08:24:56 +0000</pubDate>
		<dc:creator>windam</dc:creator>
				<category><![CDATA[Windows平台开发]]></category>
		<category><![CDATA[自动构建]]></category>

		<guid isPermaLink="false">http://www.windameister.org/blog/2011/06/12/%e8%87%aa%e5%8a%a8%e6%9e%84%e5%bb%ba%e4%ba%8c%ef%bc%9acruisecontrol-net%e9%85%8d%e7%bd%ae%e5%9f%ba%e7%a1%80/</guid>
		<description><![CDATA[在软件工程领域，CruiseControl是一个基于Java的持续集成框架。它包含了大量与持续集成有关的功能，如：邮件通知，Ant自动构建，以及支持多种源码管理工具。可以利用Web页面观察当前和以往的构建流程。它允许我们对软件开发中的很多流程进行持续的自动化控制。 CruiseControl是一个自由，开源软件，使用BSD类型的license。最早的时候，该软件是由ThoughtWorks公司的员工开发出来用于该公司自身内部项目的，随后则变成了一个独立的应用软件。 CruiseControl.NET是CruiseControl的.NET版本。还有一个Ruby版本的CruiseControl，被称为CruiseControl.rb。 对于使用Windows平台，以及Visual Studio集成开发环境的团队来说，使用CruiseControl.NET的好处在于他整合了MSBuild，NAnt，VSS等微软平台下常用的软件开发中的工具，并且非常易于在.NET平台下对他的功能进行扩展。 我们可以利用MSBuild来编写构建脚本，利用CruiseControl.NET配置实现持续集成，每日构建，自动化测试，邮件通知等功能。 安装CruiseControl.NET需要先安装.NET Framework 2.0以及支持ASP.NET的Web server（比较典型的如开启了ASP.NET的IIS服务器） CruiseControl.NET提供了两种查看和管理构建流程的方式： 1. 通过Web Dashboard，也就是通过网页查看 2. 通过CCTray客户端（该客户端是一个常驻程序，可以随时点开查看当前的构建状况，并且可以在构建成功或失败后弹出提示） 安装CruiseControl.NET 从ThoughtWorks的网站下载当前最新的Release版本： http://confluence.public.thoughtworks.org/display/CCNET/Download 在服务器上安装该CCNet安装程序（服务器需要是Windows的，并且有.NET2.0运行环境与支持ASP.NET的WebServer） 配置CruiseControl.NET： 安装目录下有名为ccnet.config的xml格式的配置文件，用于配置当前的CCNet服务器。 对于一个比较简单的项目，我们的持续集成需求包括： 1. 从源代码管理服务器上取下最新的源码 2. 构建 3. 构建成功后，将文件发布到我们指定的目录 我们还可能会需要每日构建，每天夜里0:00开始一次所有项目的构建，并将结果发布到指定位置。 项目之间往往存在互相依赖，如果被依赖项发生了更改，应该有办法让所有依赖了该项目的项目都依次构建。 下面以一个简单的例子描述如何配置CruiseControl.net服务器。 打开ccnet.config文件，该文件是一个xml格式的文件，可以在配置文件中编写多个project，每个project都对应着一个&#60;project&#62; &#60;/project&#62;块。 &#60;cruisecontrol&#62; &#60;project name=”MyProj1”&#62; &#60;/project&#62; &#60;project name=”MyProj2”&#62; &#60;/project&#62; &#60;/cruisecontrol&#62; 每个Project块的配置类似下面的例子： &#60;project name=&#8221;Project 1&#8243; queue=&#8221;Q1&#8243; queuePriority=&#8221;1&#8243;&#62; &#60;workingDirectory&#62;yourWorkingDirectory&#60;/workingDirectory&#62; &#60;artifactDirectory&#62;yourArtifactDirectory&#60;/artifactDirectory&#62; &#60;category&#62;Category 1&#60;/category&#62; &#60;webURL&#62;http://server1/ccnet/server/local/project/testProject/ViewLatestBuildReport.aspx&#60;/webURL&#62; &#60;modificationDelaySeconds&#62;2&#60;/modificationDelaySeconds&#62; &#60;maxSourceControlRetries&#62;5&#60;/maxSourceControlRetries&#62; [...]]]></description>
			<content:encoded><![CDATA[<p>在软件工程领域，CruiseControl是一个基于Java的持续集成框架。它包含了大量与持续集成有关的功能，如：邮件通知，Ant自动构建，以及支持多种源码管理工具。可以利用Web页面观察当前和以往的构建流程。它允许我们对软件开发中的很多流程进行持续的自动化控制。</p>
<p>CruiseControl是一个自由，开源软件，使用BSD类型的license。最早的时候，该软件是由ThoughtWorks公司的员工开发出来用于该公司自身内部项目的，随后则变成了一个独立的应用软件。</p>
<p>CruiseControl.NET是CruiseControl的.NET版本。还有一个Ruby版本的CruiseControl，被称为CruiseControl.rb。</p>
<p>对于使用Windows平台，以及Visual Studio集成开发环境的团队来说，使用CruiseControl.NET的好处在于他整合了MSBuild，NAnt，VSS等微软平台下常用的软件开发中的工具，并且非常易于在.NET平台下对他的功能进行扩展。</p>
<p><span id="more-272"></span></p>
<p>我们可以利用MSBuild来编写构建脚本，利用CruiseControl.NET配置实现持续集成，每日构建，自动化测试，邮件通知等功能。</p>
<p>安装CruiseControl.NET需要先安装.NET Framework 2.0以及支持ASP.NET的Web server（比较典型的如开启了ASP.NET的IIS服务器）</p>
<p>CruiseControl.NET提供了两种查看和管理构建流程的方式：</p>
<p>1. 通过Web Dashboard，也就是通过网页查看</p>
<p>2. 通过CCTray客户端（该客户端是一个常驻程序，可以随时点开查看当前的构建状况，并且可以在构建成功或失败后弹出提示）</p>
<p>安装CruiseControl.NET</p>
<p>从ThoughtWorks的网站下载当前最新的Release版本：</p>
<p><a href="http://confluence.public.thoughtworks.org/display/CCNET/Download">http://confluence.public.thoughtworks.org/display/CCNET/Download</a></p>
<p>在服务器上安装该CCNet安装程序（服务器需要是Windows的，并且有.NET2.0运行环境与支持ASP.NET的WebServer）</p>
<p>配置CruiseControl.NET：</p>
<p>安装目录下有名为ccnet.config的xml格式的配置文件，用于配置当前的CCNet服务器。</p>
<p>对于一个比较简单的项目，我们的持续集成需求包括：</p>
<p>1. 从源代码管理服务器上取下最新的源码</p>
<p>2. 构建</p>
<p>3. 构建成功后，将文件发布到我们指定的目录</p>
<p>我们还可能会需要每日构建，每天夜里0:00开始一次所有项目的构建，并将结果发布到指定位置。</p>
<p>项目之间往往存在互相依赖，如果被依赖项发生了更改，应该有办法让所有依赖了该项目的项目都依次构建。</p>
<p>下面以一个简单的例子描述如何配置CruiseControl.net服务器。</p>
<p>打开ccnet.config文件，该文件是一个xml格式的文件，可以在配置文件中编写多个project，每个project都对应着一个&lt;project&gt; &lt;/project&gt;块。</p>
<p>&lt;cruisecontrol&gt;</p>
<p>&lt;project name=”MyProj1”&gt;</p>
<p>&lt;/project&gt;</p>
<p>&lt;project name=”MyProj2”&gt;</p>
<p>&lt;/project&gt;</p>
<p>&lt;/cruisecontrol&gt;</p>
<p>每个Project块的配置类似下面的例子：</p>
<p>&lt;project name=&#8221;Project 1&#8243; queue=&#8221;Q1&#8243; queuePriority=&#8221;1&#8243;&gt;</p>
<p>&lt;workingDirectory&gt;yourWorkingDirectory&lt;/workingDirectory&gt;</p>
<p>&lt;artifactDirectory&gt;yourArtifactDirectory&lt;/artifactDirectory&gt;</p>
<p>&lt;category&gt;Category 1&lt;/category&gt;</p>
<p>&lt;webURL&gt;http://server1/ccnet/server/local/project/testProject/ViewLatestBuildReport.aspx&lt;/webURL&gt;</p>
<p>&lt;modificationDelaySeconds&gt;2&lt;/modificationDelaySeconds&gt;</p>
<p>&lt;maxSourceControlRetries&gt;5&lt;/maxSourceControlRetries&gt;</p>
<p>&lt;initialState&gt;Stopped&lt;/initialState&gt;</p>
<p>&lt;startupMode&gt;UseInitialState&lt;/startupMode&gt;</p>
<p>&lt;triggers&gt;</p>
<p>&lt;!&#8211;yourFirstTriggerType .. &#8211;&gt;</p>
<p>&lt;!&#8211;yourOtherTriggerType .. &#8211;&gt;</p>
<p>&lt;/triggers&gt;</p>
<p>&lt;!&#8211; state type=&#8221;yourStateManagerType&#8221; .. &#8211;&gt;</p>
<p>&lt;!&#8211; sourcecontrol type=&#8221;yourSourceControlType&#8221; .. &#8211;&gt;</p>
<p>&lt;!&#8211; labeller type=&#8221;yourLabellerType&#8221; .. &#8211;&gt;</p>
<p>&lt;prebuild&gt;</p>
<p>&lt;!&#8211; yourFirstPrebuildTask .. &#8211;&gt;</p>
<p>&lt;!&#8211; yourOtherPrebuildTask .. &#8211;&gt;</p>
<p>&lt;/prebuild&gt;</p>
<p>&lt;tasks&gt;</p>
<p>&lt;!&#8211; yourFirstTask .. &#8211;&gt;</p>
<p>&lt;!&#8211; yourOtherTask .. &#8211;&gt;</p>
<p>&lt;/tasks&gt;</p>
<p>&lt;publishers&gt;</p>
<p>&lt;!&#8211; yourFirstPublisherTask .. &#8211;&gt;</p>
<p>&lt;!&#8211; yourOtherPublisherTask .. &#8211;&gt;</p>
<p>&lt;/publishers&gt;</p>
<p>&lt;externalLinks&gt;</p>
<p>&lt;externalLink name=&#8221;My First Link&#8221; url=&#8221;http://somewhere/&#8221; /&gt;</p>
<p>&lt;externalLink name=&#8221;My Other Link&#8221; url=&#8221;http://somewhere.else/&#8221; /&gt;</p>
<p>&lt;/externalLinks&gt;</p>
<p>&lt;parameters&gt;</p>
<p>&lt;textParameter name=&#8221;Build Name&#8221; default=&#8221;Unknown&#8221; /&gt;</p>
<p>&lt;/parameters&gt;</p>
<p>&lt;linkedSites&gt;</p>
<p>&lt;namedValue name=&#8221;ohloh&#8221; value=&#8221;5623&#8243; /&gt;</p>
<p>&lt;/linkedSites&gt;</p>
<p>&lt;/project&gt;</p>
<p>具体的属性含义可以参考文档：</p>
<p><a href="http://confluence.public.thoughtworks.org/display/CCNET/Project+Configuration+Block">http://confluence.public.thoughtworks.org/display/CCNET/Project+Configuration+Block</a></p>
<p>对于Project块，有几个比较重要的内容值得详细说明：</p>
<p>1. Queue</p>
<p>&lt;project name=&#8221;Project 1&#8243; queue=&#8221;Q1&#8243; queuePriority=&#8221;1&#8243;&gt;</p>
<p>这里有一个queue属性，以及对应的queuePriority属性。说明的是该project被放置于哪个queue中，如果不加指明，那么每个project都会被放置到一个独立的queue中。</p>
<p>Queue的作用是什么？</p>
<p>想象一下存在两个项目A和B，并且B依赖A。如果正在构建B的时候，trigger又启动了一个Build开始构建A，但是由于B依赖A，因此在构建B的过程中，需要打开A的目标文件进行读取（例如B使用A的lib文件进行link）。如果A此时恰好要写入该目标文件，则会发生文件访问冲突。进而造成A的构建流程失败。</p>
<p>我们想到或许可以将B依赖的文件拷贝一份出来，从而避免出现使用文件和写入文件冲突的情况，但是很快我们就会意识到这也行不通——因为可能我们在拷贝文件的时候（这里需要打开并读取A的输出文件），遇到了A正要写入文件的状况。</p>
<p>总结一下，对于相互之间存在依赖的项目，我们不应该让他们不受控制的并发构建，而是必须要有一个先后顺序——这里就是Queue的用处所在。</p>
<p>CruiseControl工作的时候，是以queue为单位进行的，当trigger或者人工通过force build触发了一次构建，这次构建会被置放到queue中，并且根据queuePriority决定被插入的位置。</p>
<p>这样，可以保证一系列相互之间存在依赖关系的项目在一个队列中依次进行Build，避免因相互依赖项目同时构建而导致的文件读写冲突等问题。</p>
<p>2. Triggers</p>
<p>触发器是实现我们的各种构建需求的工具。比如，每天夜里0:00产生一次Nightly Build，比如每次Check in之后都进行一次Continuous Integration Build，比如基础项目构建成功了，应当引发所有的依赖该基础项的项目都产生一次构建。等等。</p>
<p>要实现上述的功能，我们可以使用CruiseControl.NET提供的诸多类型的trigger达到目的，或者选择其中若干进行组合使用。</p>
<p>最常用的trigger之一是<a href="http://confluence.public.thoughtworks.org/display/CCNET/Interval+Trigger">Interval Trigger</a>，我们可以设置Interval的间隔时间，每隔这么长的时间，触发器就会被触发一次。配合SCM工具对源代码的检查，我们可以利用Interval Trigger实现持续集成。（每隔1分钟触发一次，检查源码仓库是否有新checkin的代码，如果有则Build）</p>
<p><a href="http://confluence.public.thoughtworks.org/display/CCNET/Project+Trigger">Project Trigger</a>可以使得当前项目获知依赖项目构建成功的消息，当制定的Project构建成功之后，Trigger会被触发</p>
<p><a href="http://confluence.public.thoughtworks.org/display/CCNET/Multiple+Trigger">Multiple Trigger</a>用于将多个Trigger用逻辑关系组合起来使用，例如，当使用And逻辑进行组合时，Multiple Trigger中只要有一个Trigger认为不应当触发构建，就不会被触发构建。</p>
<p>3. Tasks</p>
<p>当Trigger触发之后，就会开始构建流程，从而将Tasks块的所有Task依次执行一遍。</p>
<p>我们需要使用Task来完成主要的构建流程。上篇文章中曾经简单介绍过MSBuild，到了这里，我们就利用MSBuild来实现自动构建了。</p>
<p>&lt;msbuild&gt;</p>
<p>&lt;executable&gt;C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\MSBuild.exe&lt;/executable&gt;</p>
<p>&lt;workingDirectory&gt;C:\dev\ccnet&lt;/workingDirectory&gt;</p>
<p>&lt;projectFile&gt;build.xml&lt;/projectFile&gt;</p>
<p>&lt;targets&gt;Build;Test&lt;/targets&gt;</p>
<p>&lt;timeout&gt;900&lt;/timeout&gt;</p>
<p>&lt;logger&gt;C:\Program Files\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MsBuild.dll&lt;/logger&gt;</p>
<p>&lt;/msbuild&gt;</p>
<p>最简单的利用MSBuild的方式如上例所示。</p>
<p>projectFile是预先编写好的MSBuild的脚本文件，targets里面填写的是要执行MSBuild脚本中的哪个Target。（关于Target，可以参考此前总结的MSBuild简介里的相关内容，以及MSBuild自身的文档）timeout时间是指此次build如果超过多长时间则认为是超时失败。</p>
<p>4. Publishers</p>
<p>Publisher用于在构建流程走完之后将最终的结果发布出去。Publisher中可以有一系列的Task，与Tasks块中的Task的用法类似，包括调用MSBuild Task，在MSBuild脚本中执行操作等。</p>
<p>我们可以利用Publisher模块，将最新版本的SDK发布到服务器，或是创建提供给用户的安装包等。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.windameister.org/blog/2011/06/12/autobuild_basics_on_cruisecontrol_net_cofiguration/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>自动构建一：MSBuild基础</title>
		<link>http://www.windameister.org/blog/2011/06/12/autobuild_basics_on_msbuild/</link>
		<comments>http://www.windameister.org/blog/2011/06/12/autobuild_basics_on_msbuild/#comments</comments>
		<pubDate>Sun, 12 Jun 2011 08:24:22 +0000</pubDate>
		<dc:creator>windam</dc:creator>
				<category><![CDATA[Windows平台开发]]></category>
		<category><![CDATA[自动构建]]></category>

		<guid isPermaLink="false">http://www.windameister.org/blog/2011/06/12/%e8%87%aa%e5%8a%a8%e6%9e%84%e5%bb%ba%e4%b8%80%ef%bc%9amsbuild%e5%9f%ba%e7%a1%80/</guid>
		<description><![CDATA[MSBuild是微软自Visual Studio 2005开始提供的构建平台，既可以用于VC#项目，也可用于VC++项目。 通常情况下，我们采用IDE本身进行项目的构建，有时，也会采用命令行方式调用IDE程序Build我们的项目。（对于VC++项目来说，就是通过命令行调用devenv进行构建） 然而，当我们希望在持续集成工具当中实现项目的自动构建时，尤其是当依赖情况或者部署情况较为复杂时，我们希望有一个简单、易维护的工具实现构建——MSBuild就是我们所需要的工具。 采用MSBuild进行构建，需要我们编写MSBuild脚本——一种规定格式的XML文件。 MSBuild基本组成部分包括：Target（目标），ItemGroup（项），PropertyGroup（属性），Task（任务）。 其中：Target目标是MSBuild执行的基本单元。 我们在命令行中调用MSBuild /t:TargetName指定需要执行的目标。 每个Target中可以包含若干Task。 Task是基本的功能单元：例如拷贝文件，删除文件，创建/删除目录，使用Exec调用外部程序，调用VCBuild构建.vcproj项目，调用Csc编译.cs文件，等等。此外，你还可以通过实现ITask接口，创建自定义的Task。 PropertyGroup作为属性，起着类似于变量一样的作用，可以定义一个属性，并在后续的位置使用该属性，例如： &#60;PropertyGroup&#62; &#60;ProjFile&#62;C:\Test\test.vcproj&#60;/ProjFile&#62; &#60;Rebuild&#62;true&#60;/Rebuild&#62; &#60;/PropertyGroup&#62; &#60;Target Name=”Build”&#62; &#60;VCBuild     Projects=”$(ProjFile)”     Rebuild=”$(Rebuild)”    /&#62; &#60;/Target&#62; ItemGroup往往作为输入，例如： &#60;ItemGroup&#62; &#60;IncFiles Include=”C:\Test\*.h” /&#62; &#60;/ItemGroup&#62; &#60;Target Name=”Build”&#62; &#60;Copy    SourceFiles=”@(IncFiles)”    DestinationFolder=”c:\output\include” /&#62; &#60;/Target&#62; &#160; 实例： 假设我们有一个项目A，依赖一个动态连接库B，我们需要先构建B，将B的头文件和lib文件拷贝到A的包含头文件路径和Lib路径下，再构建A，然后将A发布到指定的路径下。 B的项目结构如下： C:\B\B.vcproj A的项目结构如下： C:\A \DepInclude \DepLib \Src\A.vcproj \Output 对应的MSBuild脚本如下： &#60;ItemGroup&#62; &#60;BIncFiles Include=”C:\B\*.h” /&#62; &#60;BLibFiles Include=”C:\B\Release\*.lib” [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://msdn.microsoft.com/zh-cn/library/ms171451(v=vs.80).aspx">MSBuild</a>是微软自Visual Studio 2005开始提供的构建平台，既可以用于VC#项目，也可用于VC++项目。</p>
<p>通常情况下，我们采用IDE本身进行项目的构建，有时，也会采用命令行方式调用IDE程序Build我们的项目。（对于VC++项目来说，就是通过命令行调用devenv进行构建）</p>
<p>然而，当我们希望在持续集成工具当中实现项目的自动构建时，尤其是当依赖情况或者部署情况较为复杂时，我们希望有一个简单、易维护的工具实现构建——MSBuild就是我们所需要的工具。</p>
<p><span id="more-271"></span></p>
<p>采用MSBuild进行构建，需要我们编写MSBuild脚本——一种规定格式的XML文件。</p>
<p>MSBuild基本组成部分包括：Target（目标），ItemGroup（项），PropertyGroup（属性），Task（任务）。</p>
<p>其中：Target目标是MSBuild执行的基本单元。</p>
<p>我们在命令行中调用MSBuild /t:TargetName指定需要执行的目标。</p>
<p>每个Target中可以包含若干Task。</p>
<p>Task是基本的功能单元：例如拷贝文件，删除文件，创建/删除目录，使用Exec调用外部程序，调用VCBuild构建.vcproj项目，调用Csc编译.cs文件，等等。此外，你还可以通过实现ITask接口，创建自定义的Task。</p>
<p>PropertyGroup作为属性，起着类似于变量一样的作用，可以定义一个属性，并在后续的位置使用该属性，例如：</p>
<div class="codearea">
<pre>&lt;PropertyGroup&gt;
   &lt;ProjFile&gt;C:\Test\test.vcproj&lt;/ProjFile&gt;
   &lt;Rebuild&gt;true&lt;/Rebuild&gt;
&lt;/PropertyGroup&gt;

&lt;Target Name=”Build”&gt;
   &lt;VCBuild     Projects=”$(ProjFile)”     Rebuild=”$(Rebuild)”    /&gt;
&lt;/Target&gt;</pre>
</div>
<p>ItemGroup往往作为输入，例如：</p>
<div class="codearea">
<pre>&lt;ItemGroup&gt;
   &lt;IncFiles Include=”C:\Test\*.h” /&gt;
&lt;/ItemGroup&gt;

&lt;Target Name=”Build”&gt;
   &lt;Copy    SourceFiles=”@(IncFiles)”    DestinationFolder=”c:\output\include”
/&gt;
&lt;/Target&gt;</pre>
</div>
<p><!--.codearea{ color:black;  background-color:white;  line-height:18px;  border:1px solid #4f81bd;  margin:0;  width:auto !important;  width:100%;  overflow:auto;  text-align:left;  font-size:12px;  font-family: "Courier New","Consolas","Fixedsys","BitStream Vera Sans Mono", courier,monospace,serif} .codearea pre{ color:black; line-height:18px;  padding:0 0 0 12px !important; margin:0em;  background-color:#fff !important} .linewrap pre{white-space:pre-wrap;  white-space:-moz-pre-wrap;  white-space:-pre-wrap;  white-space:-o-pre-wrap;  word-wrap:break-word;  word-break:normal} .codearea pre.alt{ background-color:#f7f7ff !important} .codearea .lnum{color:#4f81bd;line-height:18px} -->&nbsp;</p>
<p>实例：</p>
<p>假设我们有一个项目A，依赖一个动态连接库B，我们需要先构建B，将B的头文件和lib文件拷贝到A的包含头文件路径和Lib路径下，再构建A，然后将A发布到指定的路径下。</p>
<p>B的项目结构如下：</p>
<p>C:\B\B.vcproj</p>
<p>A的项目结构如下：</p>
<p>C:\A</p>
<p>\DepInclude</p>
<p>\DepLib</p>
<p>\Src\A.vcproj</p>
<p>\Output</p>
<p>对应的MSBuild脚本如下：</p>
<div class="codearea">
<pre>&lt;ItemGroup&gt;
   &lt;BIncFiles Include=”C:\B\*.h” /&gt;
   &lt;BLibFiles Include=”C:\B\Release\*.lib” /&gt;
   &lt;BDllFiles Include=”C:\B\Release\*.dll” /&gt;
&lt;/ItemGroup&gt;

&lt;TargetName=”Build_B”&gt;
    &lt;VCBuild
      Projects=”C:\B\B.vcproj”
      Configuration=”Release”
      Rebuild=”true”     /&gt;
&lt;Copy   SourceFiles=”@(BIncFiles)”
  DestinationFolder=”C:\A\DepInclude”
/&gt;
&lt;Copy   SourceFiles=”@(BLibFiles)”
  DestinationFolder=”C:\A\DepLib”
/&gt;
&lt;/Target&gt;

&lt;Target Name=“Build_A” DependOnTargets=“Build_B”&gt;
    &lt;VCBuild
      Projects=”C:\A\Src\A.vcproj“
      Configuration=“Release”
      Rebuild=”true”
    /&gt;
&lt;/Target&gt;</pre>
</div>
<p><!--.codearea{ color:black;  background-color:white;  line-height:18px;  border:1px solid #4f81bd;  margin:0;  width:auto !important;  width:100%;  overflow:auto;  text-align:left;  font-size:12px;  font-family: "Courier New","Consolas","Fixedsys","BitStream Vera Sans Mono", courier,monospace,serif} .codearea pre{ color:black; line-height:18px;  padding:0 0 0 12px !important; margin:0em;  background-color:#fff !important} .linewrap pre{white-space:pre-wrap;  white-space:-moz-pre-wrap;  white-space:-pre-wrap;  white-space:-o-pre-wrap;  word-wrap:break-word;  word-break:normal} .codearea pre.alt{ background-color:#f7f7ff !important} .codearea .lnum{color:#4f81bd;line-height:18px} -->&nbsp;</p>
<p>Build_A的Target使用了一个DependOnTargets属性，该属性的含义是当前Target要依赖前续的某些Targets，那些依赖的Targets必须先执行，然后才能执行当前的Target。</p>
<p>在本例中Build_B会rebuild整个B工程，并将结果的头文件和lib文件拷贝到A项目的依赖路径当中。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.windameister.org/blog/2011/06/12/autobuild_basics_on_msbuild/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>从C++到C#——穿行于Native与.Net 托管代码之间</title>
		<link>http://www.windameister.org/blog/2010/12/19/from-cpp-to-csharp-cross-the-boundary-of-native-and-managed-code/</link>
		<comments>http://www.windameister.org/blog/2010/12/19/from-cpp-to-csharp-cross-the-boundary-of-native-and-managed-code/#comments</comments>
		<pubDate>Sun, 19 Dec 2010 09:52:55 +0000</pubDate>
		<dc:creator>windam</dc:creator>
				<category><![CDATA[C++ & Design]]></category>
		<category><![CDATA[Windows平台开发]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[托管代码]]></category>

		<guid isPermaLink="false">http://www.windameister.org/blog/?p=245</guid>
		<description><![CDATA[微软在.Net平台中提供了大量丰富的语言，库，并为之提供了非常强大的开发工具。借用.Net平台，我们可以非常容易的开发基于Windows的应用，与采用C++相比，使用C#语言开发相同功能的软件可以极大的提升开发效率，缩短开发周期，减少Bug。 然而，往往现存有大量的积累代码是基于原生语言开发的，当我们决定采用C#时，必须考虑到与现存的软件组件交互的能力，具体而言——如何在一个以C++语言为主的项目中引入.NET组件？如何在.NET组件中使用C++代码库提供的功能？ 在游戏项目中，比较理想的情况是底层的3D引擎采用C++编写，而上层编辑器则采用C#编写，这样可以兼顾引擎的运行效率和编辑器的开发效率。一个值得一提的案例是UDK（Unreal引擎的开发包），其中就采用了C++语言与.NET组件相结合的模式，其界面上相当一部分新功能是采用.NET开发的。 下面介绍下我总结的C++与.NET互操作的方法： 1. C++ –&#62; .NET在C++语言中调用.NET 由于.NET组件被设计成COM兼容格式，我们可以像使用COM组件一样使用.NET组件。也就是说，我们可以创建.NET组件中的COM对象，并获取接口来使用它。 值得注意的是使用.NET编译出的dll模块，与采用Native语言构建的ActiveX模块或者其他COM DLL是有差别的，你无法使用regsvr32对其进行注册（没有导出DllRegisterServer, DllUnregisterServer）。 &#160; 如果希望在C++中调用.NET代码，那么在实现.NET组件时，需要设置ComVisible属性，具体而言，就是在Properties\AssemblyInfo.cs 中，将[assembly: ComVisible(false)]（默认）属性，更改为： [assembly: ComVisible(true)]，这样才能使用COM的方式访问到.NET组件中的接口。 将需要暴露给Native代码使用的功能抽象成接口，在.NET中实现该接口，并为之提供Guid（可以采用VC2005中的工具Tools/Create Guid），这样才能在Native代码中使用该接口，并用Guid创建这个接口的实例。例如下面这样： namespace XDll { [Guid("1DE25FAA-4AA1-4819-B1FF-0D507567685E ")] public interface IXSystem { } [Guid("603F977B-34CF-40ca-BB30-B6BC25D2A370")] public class ImplXSystem : IXSystem { } } 在.NET组件构建完成后，需要使用regasm工具，将其中的COM接口导出成tlb文件，以供给C++代码使用。Regasm工具通常安装在.NET Framework的目录下，例如： &#8220;C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\regasm&#8221; 命令行如下： &#8220;C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\regasm&#8221; X.dll /tlb:X.tlb 这里注意一点，安装.NET framework并不会自动将上述路径加入到系统路径中，因此运行regasm时需要键入完整的路径。 在Native代码中调用.NET代码时（VC2005开发环境），需要做以下几件事： #import &#60;mscorlib.tlb&#62; raw_interfaces_only #import “X.tlb” no_namespace [...]]]></description>
			<content:encoded><![CDATA[<p>微软在.Net平台中提供了大量丰富的语言，库，并为之提供了非常强大的开发工具。借用.Net平台，我们可以非常容易的开发基于Windows的应用，与采用C++相比，使用C#语言开发相同功能的软件可以极大的提升开发效率，缩短开发周期，减少Bug。</p>
<p>然而，往往现存有大量的积累代码是基于原生语言开发的，当我们决定采用C#时，必须考虑到与现存的软件组件交互的能力，具体而言——如何在一个以C++语言为主的项目中引入.NET组件？如何在.NET组件中使用C++代码库提供的功能？</p>
<p>在游戏项目中，比较理想的情况是底层的3D引擎采用C++编写，而上层编辑器则采用C#编写，这样可以兼顾引擎的运行效率和编辑器的开发效率。一个值得一提的案例是<a href="http://www.udk.com/">UDK</a>（Unreal引擎的开发包），其中就采用了C++语言与.NET组件相结合的模式，其界面上相当一部分新功能是采用.NET开发的。</p>
<p><span id="more-245"></span></p>
<p>下面介绍下我总结的C++与.NET互操作的方法：</p>
<p><strong>1. C++ –&gt; .NET在C++语言中调用.NET</strong></p>
<p>由于.NET组件被设计成COM兼容格式，我们可以像使用COM组件一样使用.NET组件。也就是说，我们可以创建.NET组件中的COM对象，并获取接口来使用它。</p>
<p>值得注意的是使用.NET编译出的dll模块，与采用Native语言构建的ActiveX模块或者其他COM DLL是有差别的，你无法使用regsvr32对其进行注册（没有导出DllRegisterServer, DllUnregisterServer）。</p>
<p>&nbsp;</p>
<p>如果希望在C++中调用.NET代码，那么在实现.NET组件时，需要设置ComVisible属性，具体而言，就是在Properties\AssemblyInfo.cs 中，将[assembly: ComVisible(false)]（默认）属性，更改为：</p>
<p>[assembly: ComVisible(true)]，这样才能使用COM的方式访问到.NET组件中的接口。</p>
<p>将需要暴露给Native代码使用的功能抽象成接口，在.NET中实现该接口，并为之提供Guid（可以采用VC2005中的工具Tools/Create Guid），这样才能在Native代码中使用该接口，并用Guid创建这个接口的实例。例如下面这样：</p>
<div class="codearea">
<pre><span style="color: blue;">namespace </span><span style="color: #010001;">XDll
</span>{
    [<span style="color: #010001;">Guid</span>(<span style="color: #a31515;">"1DE25FAA-4AA1-4819-B1FF-0D507567685E "</span>)]
    <span style="color: blue;">public interface </span><span style="color: #010001;">IXSystem
    </span>{

    }

    [<span style="color: #010001;">Guid</span>(<span style="color: #a31515;">"603F977B-34CF-40ca-BB30-B6BC25D2A370"</span>)]
    <span style="color: blue;">public class </span><span style="color: #010001;">ImplXSystem </span>: <span style="color: #010001;">IXSystem
    </span>{ 

    }
}</pre>
</div>
<p>在.NET组件构建完成后，需要使用<a href="http://msdn.microsoft.com/zh-cn/library/tzat5yw6(v=vs.80).aspx">regasm</a>工具，将其中的COM接口导出成tlb文件，以供给C++代码使用。Regasm工具通常安装在.NET Framework的目录下，例如：</p>
<p>&#8220;C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\regasm&#8221;</p>
<p>命令行如下：</p>
<p>&#8220;C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\regasm&#8221; X.dll /tlb:X.tlb</p>
<p>这里注意一点，安装.NET framework并不会自动将上述路径加入到系统路径中，因此运行regasm时需要键入完整的路径。</p>
<p>在Native代码中调用.NET代码时（VC2005开发环境），需要做以下几件事：</p>
<p>#import &lt;mscorlib.tlb&gt; raw_interfaces_only</p>
<p>#import “X.tlb” no_namespace named_guids</p>
<p>这两行将一些必要的接口导入，X.tlb是前述步骤中使用regasm工具导出的文件，其中包含了使用C++语言描述的.NET组件中的接口。</p>
<p>通常情况下，Visual Studio会在$CURRENTUSER$\local settings\temp\目录下生成一个后缀名为.tlh的文件，其中包含了.NET库导出的诸多CLSID_X以及IID_X等GUID，以及各COM接口，C++代码可以直接使用该文件中的GUID与接口声明。</p>
<p>接下来使用.NET组件接口与使用COM接口基本相同：</p>
<div class="codearea">
<pre><span style="color: #010001;">CoInitialize</span>(<span style="color: #010001;">NULL</span>); <span style="color: green;">// 初?始?化?COM相?关?

</span><span style="color: #010001;">IX</span>* <span style="color: #010001;">pIX</span>;
<span style="color: #010001;">CoCreateInstance</span>(<span style="color: #010001;">CLSID_X
                 </span>, <span style="color: #010001;">NULL
                 </span>, <span style="color: #010001;">CLSCTX_INPROC_SERVER
                 </span>, <span style="color: #010001;">IID_IX
                 </span>, <span style="color: blue;">reinterpret_cast</span>&lt;<span style="color: blue;">void</span>**&gt;(&amp;<span style="color: #010001;">pIX</span>));</pre>
</div>
<p>&nbsp;</p>
<p>注意这里的CLSID前缀的Guid与IID前缀的Guid的区别，前者是实现类的Guid，后者是接口的Guid，我们这里调用CoCreateInstance的意义在于，创建一个Guid为CLSID_X的实现类，并返回给我一个Guid为IID_IX的接口，我将会以这个接口去访问该实现类。</p>
<p>这里的CLSID_X与IID_IX都是在导出tlb文件时自动生成的GUID数据结构，可以在Native代码中直接使用。</p>
<p>当使用了.NET组件的程序被发布时，需要将上述tlb中的各种ID注册到目标机器的注册表的相应键值中。比如，CLSID就需要导入到目标机器的注册表的HKEY_CLASSES_ROOT\CLSID键中。这一步可以通过regasm的/regfile功能实现，该功能可以导出所有需要的信息到注册表中。</p>
<p>最后当使用完毕之后，记得需要调用创建的COM接口的Release()方法。</p>
<p>pIX-&gt;Release();</p>
<p>以及清理：</p>
<p>CoUninitialize();</p>
<p><strong>2. .NET组件调用Native代码(C++)</strong></p>
<p>由于.NET组件是出现于C++之后，微软在设计时就已经基于COM模型设计其二进制接口，因此很容易可以让以前的原生语言来调用，而C++语言则诞生得更早，没有定义二进制标准，要想让.NET组件调用，只能把自己实现成COM组件的模式。关于如何用Native语言实现一个COM组件，可以参考更加专业的参考书：<a href="http://book.douban.com/subject/1231596/">《COM技术内幕》</a>，<a href="http://book.douban.com/subject/1231481/">《COM本质论》</a>等。本文主要提及在实现COM接口时值得考虑的两种思路。</p>
<p>根据实际应用场景的不同，我们可能面临不同的技术选择：</p>
<p>场景1： <strong>.NET组件依赖于C++库，完全基于C++库构建，并且C++模块不会反向依赖.NET组件</strong></p>
<p>这种情形下，由于依赖结构简单，我们可以将整个C++库采用C++CLI封装，有一些开源库采用了这种做法，比如<a href="http://slimdx.org/">SlimDX</a>项目（这是一个为了可以在.NET中使用DirectX的项目，将DirectX采用C++CLI封装，最终提供了一套面向.NET的开发框架。）再如ManagedOgre或者OgreDotNet，都将Ogre引擎封装在C++CLI中从而提供在.NET中操作OGRE的方案。</p>
<p>场景2： <strong>.NET组件依赖于C++模块，C++模块又依赖.NET组件</strong></p>
<p>.NET中需要调用底层的一些服务做一些事情，例如采用底层接口绘制一个三维场景，置放于.NET模块的窗口中。同时可能C++模块又要依赖.NET组件，比如一个.NET控件需要在一个以C++语言为主开发的编辑器中被调用。</p>
<p>形成类似于下面这样的关系，Native Code与Managed Code互相依赖：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/12/image.png"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border-width: 0px;" title="image" src="http://www.windameister.org/blog/wp-content/uploads/2010/12/image_thumb.png" border="0" alt="image" width="547" height="147" /></a></p>
<p>这里，我们可以将.NET对C++模块的依赖提取成一个.NET接口，而后C++代码实现该接口，从而消除.NET对C++代码的直接依赖，将之转变成C++模块对.NET接口的依赖。这是一种在重构中经常使用的手法。</p>
<p>举一个简单的例子来说明这个问题：</p>
<div class="codearea">
<pre><span style="color: blue;">namespace </span><span style="color: #010001;">XDLL
</span>{
    <span style="color: blue;">interface </span><span style="color: #010001;">XInterface
    </span>{
        <span style="color: blue;">void </span><span style="color: #010001;">Foo</span>();
    };

    <span style="color: blue;">public class </span><span style="color: #010001;">XModule
    </span>{
        <span style="color: blue;">private </span><span style="color: #010001;">XInterface ix</span>;

        <span style="color: #010001;">XModule</span>(<span style="color: #010001;">XInterface ix</span>)
        {
            <span style="color: blue;">this</span>.<span style="color: #010001;">ix </span>= <span style="color: #010001;">ix</span>;
        }

        <span style="color: blue;">void </span><span style="color: #010001;">Foo</span>()
        {
            <span style="color: blue;">this</span>.<span style="color: #010001;">ix</span>.<span style="color: #010001;">Foo</span>();
        }
    }
}</pre>
</div>
<p>假设我们的C#模块中有一个类XModule，需要使用C++代码提供的某种功能，我们不直接调用C++代码（这产生一个.NET到C++的直接依赖），而是调用一个自己定义的接口XInterface，这个接口会被暴露给外部，C++代码实现该接口，并在初始化过程中传入.NET组件。</p>
<p>将上述依赖关系变成下面这样：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/12/image1.png"><img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border-width: 0px;" title="image" src="http://www.windameister.org/blog/wp-content/uploads/2010/12/image_thumb1.png" border="0" alt="image" width="537" height="270" /></a></p>
<p>## 理想的情况当然是引擎用C++实现，而后底层提供一个C++CLI的封装，这样编辑器以及上层控件都采用C#开发，既可以兼顾引擎部分的运行效率与编辑器的开发效率，这样就可以保证依赖关系始终是上层编辑器直接依赖引擎。但是如果遇到某些现实状况，例如编辑器已经由C++开发，后期的部分控件界面期望采用C#开发，这样就需要采用类似于上文中的方案，将依赖关系反过来处理。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.windameister.org/blog/2010/12/19/from-cpp-to-csharp-cross-the-boundary-of-native-and-managed-code/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>在3D游戏中采用场景管理的意义</title>
		<link>http://www.windameister.org/blog/2010/11/14/value-of-scene-management-in-3d-game/</link>
		<comments>http://www.windameister.org/blog/2010/11/14/value-of-scene-management-in-3d-game/#comments</comments>
		<pubDate>Sun, 14 Nov 2010 13:13:45 +0000</pubDate>
		<dc:creator>windam</dc:creator>
				<category><![CDATA[3D渲染]]></category>
		<category><![CDATA[网络游戏]]></category>
		<category><![CDATA[3d]]></category>
		<category><![CDATA[场景管理]]></category>

		<guid isPermaLink="false">http://www.windameister.org/blog/2010/11/14/%e5%9c%a83d%e6%b8%b8%e6%88%8f%e4%b8%ad%e9%87%87%e7%94%a8%e5%9c%ba%e6%99%af%e7%ae%a1%e7%90%86%e7%9a%84%e6%84%8f%e4%b9%89/</guid>
		<description><![CDATA[我们常说3D引擎应当包含若干功能：材质，模型，动画等等，这些功能我们很好理解，模型是我们需要渲染的几何体，材质表现的是几何体如何对光照如何做出回应，动画（特别的，骨骼动画）往往是3D世界中可运动模型的基础。除了这些与渲染直接关联的概念之外，往往还有个与渲染看似无关的概念：场景管理。 游戏渲染当中常用的场景管理往往被称为Scene Graph：它的实现通常采用树状结构，构成它的基本单位是节点，一个节点可以有若干个子节点，并且（除了根节点之外）往往有且只有一个父节点。 在实际游戏当中，对于需要渲染的几何体，比如静态的Mesh或者有Skeleton的Mesh，其本身不包含世界变换信息，而是由其所处的节点决定其位置与朝向。很久以前读Ogre的源码看到它就是这么做的，然而问题是，为什么必须要这么做？我一直心存疑惑，如果不采用Scene Graph的方案，我们给每一个需要渲染的模型赋予世界变换信息，那么这个模型不是一样可以渲染么？为什么非要把变换信息与模型本身分离开？分开有什么好处？而不这样做又有什么缺点呢？ 以下是我对该问题做的小结： 问题1：视锥裁剪 采用了Scene Graph，提供层级结构之后，就可以高效的进行视锥裁剪：（更新父节点的包围盒时需要囊括其所有子节点的包围盒）如果父节点无法被看见，那么其所有子节点也都无法被看见。这样，如果一个节点有繁多（数以百计）的子孙节点，我们可以节约很多计算。 如果没有层级结构——我们采用线性表之类的数据结构，那么需要依此遍历所有物体，并依次根据其包围盒判断可见性。 这两个方案的优劣在场景不够复杂时很难分辨，但是显而易见，随着场景复杂度的增加，场景中的物体树N越大，采用层级结构的优势就越明显。 问题1衍生：碰撞检测支持 碰撞检测是比较耗时的算法，在游戏当中，如果一次碰撞检测需要计算的物体太多，会极大的影响效率，因此，与渲染时做可见性判断类似，碰撞检测也需要在进行计算之前尽可能多的剔除无关物体。 采用层级结构的场景，可以为碰撞检测提供天然的支持，如果父节点不与物体相交，那么自然其所有子节点都不会与之相交。 问题2：资源共享 有时候我们会遇到这种需求：需要在Pos[1], Pos[2], … , Pos[N]处渲染同一个Mesh，Mesh的几何数据是一样的，而变换矩阵不同。（这里不考虑Instancing的方案） 如果没有采用Scene Graph的分离变换信息与几何数据的方案，那么我们会遇到一个尴尬的局面：重复。 下面一个例子摘自SceneGraph的Wiki，生动的描述了这个问题： For instance, a game might define a logical relationship between a knight and a horse so that the knight is considered an extension to the horse. The scene graph would have a [...]]]></description>
			<content:encoded><![CDATA[<p>我们常说3D引擎应当包含若干功能：材质，模型，动画等等，这些功能我们很好理解，模型是我们需要渲染的几何体，材质表现的是几何体如何对光照如何做出回应，动画（特别的，骨骼动画）往往是3D世界中可运动模型的基础。除了这些与渲染直接关联的概念之外，往往还有个与渲染看似无关的概念：场景管理。</p>
<p>游戏渲染当中常用的场景管理往往被称为Scene Graph：它的实现通常采用树状结构，构成它的基本单位是节点，一个节点可以有若干个子节点，并且（除了根节点之外）往往有且只有一个父节点。</p>
<p>在实际游戏当中，对于需要渲染的几何体，比如静态的Mesh或者有Skeleton的Mesh，其本身不包含世界变换信息，而是由其所处的节点决定其位置与朝向。很久以前读Ogre的源码看到它就是这么做的，然而问题是，为什么必须要这么做？我一直心存疑惑，如果不采用Scene Graph的方案，我们给每一个需要渲染的模型赋予世界变换信息，那么这个模型不是一样可以渲染么？为什么非要把变换信息与模型本身分离开？分开有什么好处？而不这样做又有什么缺点呢？</p>
<p><span id="more-237"></span></p>
<p>以下是我对该问题做的小结：</p>
<p><strong>问题1</strong><strong>：视锥裁剪</strong></p>
<p>采用了Scene Graph，提供层级结构之后，就可以高效的进行视锥裁剪：（更新父节点的包围盒时需要囊括其所有子节点的包围盒）如果父节点无法被看见，那么其所有子节点也都无法被看见。这样，如果一个节点有繁多（数以百计）的子孙节点，我们可以节约很多计算。</p>
<p>如果没有层级结构——我们采用线性表之类的数据结构，那么需要依此遍历所有物体，并依次根据其包围盒判断可见性。</p>
<p>这两个方案的优劣在场景不够复杂时很难分辨，但是显而易见，随着场景复杂度的增加，场景中的物体树N越大，采用层级结构的优势就越明显。</p>
<p><strong>问题1</strong><strong>衍生：碰撞检测支持</strong></p>
<p>碰撞检测是比较耗时的算法，在游戏当中，如果一次碰撞检测需要计算的物体太多，会极大的影响效率，因此，与渲染时做可见性判断类似，碰撞检测也需要在进行计算之前尽可能多的剔除无关物体。</p>
<p>采用层级结构的场景，可以为碰撞检测提供天然的支持，如果父节点不与物体相交，那么自然其所有子节点都不会与之相交。</p>
<p><strong>问题2</strong><strong>：资源共享</strong></p>
<p>有时候我们会遇到这种需求：需要在Pos[1], Pos[2], … , Pos[N]处渲染同一个Mesh，Mesh的几何数据是一样的，而变换矩阵不同。（这里不考虑Instancing的方案）</p>
<p>如果没有采用Scene Graph的分离变换信息与几何数据的方案，那么我们会遇到一个尴尬的局面：重复。</p>
<p>下面一个例子摘自<a href="http://en.wikipedia.org/wiki/Scene_graph">SceneGraph的Wiki</a>，生动的描述了这个问题：</p>
<blockquote><p>For instance, a game might define a logical relationship between a knight and a horse so that the knight is considered an extension to the horse. The scene graph would have a &#8216;horse&#8217; node with a &#8216;knight&#8217; node attached to it.</p>
<p>As well as describing the logical relationship, the scene graph may also describe the spatial relationship of the various entities: the knight moves through 3D space as the horse moves.</p>
<p>In these large applications, memory requirements are major considerations when designing a scene graph. For this reason many large scene graph systems use instancing to reduce memory costs and increase speed. <strong>In our example above, each knight is a separate scene node, but the graphical representation of the knight (made up of a 3D mesh, textures, materials and shaders) is instanced. This means that only a single copy of the data is kept, which is then referenced by any &#8216;knight&#8217; nodes in the scene graph. This allows a reduced memory budget and increased speed, since when a new knight node is created, the appearance data does not need to be duplicated.</strong></p></blockquote>
<p>可能有人会说：为什么不能在一个循环里依次设置世界矩阵，并绘制Mesh？这样就不会有重复。</p>
<p>是的，如果我们可以只针对这一个Mesh渲染的话，自然没有问题——但情况往往是，我们会预先注册很多个Mesh，最后一次性批量渲染（以尽可能最小化渲染状态切换的次数）。这样就要求我们注册Mesh的时候，附带着把Mesh的Transform信息也一起带着——如果不采用SceneGraph的方案，头疼的事情来了，这个Transform信息应该存放在哪？如果把它放在Mesh里：比如Mesh有一个属性说明自己在世界空间中的位置朝向 ，那么这个Mesh就被绑定到这个位置上了！绘制N个不同位置的Mesh就需要创建N个不同的Mesh实例！这在3D渲染中是无法想象的资源浪费。最后，我们还是不得不想办法把Mesh的Transform信息与Geometry数据分开。</p>
<p>SceneGraph是如何解决问题的？很简单，由于不同的Node可以挂接同一个Mesh，这样自然就实现的几何数据，乃至纹理，材质的共享。注册渲染的时候把Node注册到渲染器里就万事大吉了。</p>
<p>上述两个问题我认为是在3D游戏中采用SceneGraph的最重要的理由。</p>
<p>另，以下摘译自《<a href="http://book.douban.com/subject/3554163/">3D Game Engine Design</a>》一书：</p>
<blockquote><p>采用场景图组织游戏内容，对于游戏来说非常重要，以下是原因：</p>
<p>1. 需要管理的数据通常很大，并且是有艺术家通过小片小片的形式建造出来的。关卡编辑者可以将一整个关卡的内容，通过树结构聚合起来，则自然的由此结构造出了整体性的结构。譬如，场景中的一盏灯可以只照亮场景图中的某个子树。关卡编辑者的责任就是将该灯赋予场景图中的某个节点，该灯的效果就由场景管理系统来维护。</p>
<p>2. 层次组织结构提供了一个局部性的模式：通常游戏中存在当前交互的对象，都处在相同的空间范围内。场景图可以使得游戏程序快速的排除游戏的其他区域，从而加速后续的处理。尽管将尽可能少的数据传送至显卡是让游戏运行效率提高的主要目标，但是聚焦处理小块的数据，也对碰撞检测尤为重要。如果潜在可碰撞物体特别多的话，那么碰撞检测模块也会变得非常慢。场景图可以将一组潜在需碰撞的物体组织起来，这些物体仅在当前游戏交互区域内被考虑进来。</p>
<p>3. 很多物体天然就被用层次结构搭建起来：譬如绝大多数类人体的骨架系统。手部的位置和朝向，自然的决定于腕部，肘部以及肩部的位置和朝向。</p>
<p>4. 有持久化需要的游戏，有的时候玩家需要在某一时刻将游戏状态储存起来，并在将来从此状态继续。层次化组织结构可以使得存储世界的状态非常简单：要求根节点存储自己，然后后续的就是递归的存储每一个节点即可。</p></blockquote>
<p><strong>关于Scene Graph </strong><strong>的实现：</strong></p>
<p>SceneGraph的好处众多，但重要的问题是，维护这样一个数据结构需要的代价有多大？</p>
<p>最简单的Scene Graph就直接采用线性表，渲染，碰撞检测，都采取线性遍历即可，维护也极其简单，一个Node的移动旋转不会对其他Node产生任何影响（因为没有父子关系），因此也无需维护。对于小规模的场景，这种方案是可以接受的。但是当场景规模增大时，在可见性判断，碰撞检测等问题上，线性表的实现就表现出很大的局限性了。</p>
<p>因此比较常见的SceneGraph实现是一棵树，由Node构成，每个Node都可以挂接可渲染对象Object，并且可以有多个子Node，有且只有一个Parent Node。Node上挂接的可渲染对象决定了该Node的包围体。</p>
<p>当有Node的位置移动/朝向改变时（位于其局部坐标系内）一方面，从该Node出发，其父节点直至根节点的包围体都需要更新；另一方面，其所有的子孙节点的世界变换也需要更新以适应该节点的新位置朝向信息。</p>
<p>可能有不止一处改变在SceneGraph中的不同Node上同时发生，SceneGraph管理机制应当保证仅维护必要的节点：举例而言，如果A，B两个节点都有局部坐标变换发生，并且如果B是A的子孙节点，那么更新A的子树会自动更新B的子树，因此如果我们先更新B子树，再更新A子树，则会导致多余的计算，造成不必要的效率损失。</p>
<p>参考资料：</p>
<p>1.《3D Game Engine Desion》 Eberly, D.H <a href="http://book.douban.com/subject/3554163/">http://book.douban.com/subject/3554163/</a></p>
<p>2. Wiki SceneGraph <a href="http://en.wikipedia.org/wiki/Scene_graph">http://en.wikipedia.org/wiki/Scene_graph</a></p>
<p>3. <a href="http://www.gamerendering.com/category/scene-management/">http://www.gamerendering.com/category/scene-management/</a></p>
]]></content:encoded>
			<wfw:commentRss>http://www.windameister.org/blog/2010/11/14/value-of-scene-management-in-3d-game/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>D3D基础 &#8211; 光照，材质与着色#1</title>
		<link>http://www.windameister.org/blog/2010/09/18/d3dbasic-lighting-material-shading/</link>
		<comments>http://www.windameister.org/blog/2010/09/18/d3dbasic-lighting-material-shading/#comments</comments>
		<pubDate>Sat, 18 Sep 2010 04:22:25 +0000</pubDate>
		<dc:creator>windam</dc:creator>
				<category><![CDATA[3D渲染]]></category>
		<category><![CDATA[shader]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[d3d]]></category>
		<category><![CDATA[lighting]]></category>
		<category><![CDATA[pileline]]></category>
		<category><![CDATA[shading]]></category>

		<guid isPermaLink="false">http://www.windameister.org/blog/2010/09/18/d3d%e5%9f%ba%e7%a1%80-%e5%85%89%e7%85%a7%ef%bc%8c%e6%9d%90%e8%b4%a8%e4%b8%8e%e7%9d%80%e8%89%b2/</guid>
		<description><![CDATA[初接触d3d时，相信许多初学者和我一样，虽然对3D实时渲染的原理有所了解，但是却对整体的管线结构有所困惑，包括可能许多做3d游戏的程序员，由于常常采用封装好的引擎做上层逻辑开发，因此对底层的架构也未必了解得十分透彻。 长久以来，对3d图形学的底层技术，我一直也是只了解个只鳞半爪而已，最近一段时间，在工作中不断的接触到这方面的知识，才有了比较系统的思考与总结，本文记叙了我学习dx管线的一些思考，由于水平所限以及尚未对最新的DX11架构有所关注，所以本文不涵盖DX11的最新架构，只涉及DX9及以前的知识结构，虽然如此，但是相信如果系统的掌握了这块知识，对了解后续的新技术也是有所帮助的。 本文主要是针对对3D渲染知识有一定了解，然而对D3D的结构却不是很清楚的同学，把DX8/9的管线结构做了一番说明，将固定管线的光照、纹理混合与可编程管线加以对比，期望对这一块知识有一个比较完整的总结。 &#160; 目录 1. 硬件加速的3D渲染 2. 材质与光照 3. 变换，光照与顶点着色器 4. 纹理与像素着色器 5. Alpha测试，深度测试与Alpha混合 6. 更多话题 1. 硬件加速的3D渲染 硬件加速的3D渲染，早些时候只是被称为Hardware Transform &#38; Lighting的加速技术，这些技术是相对于早期时候，图形硬件尚未普及，软件实现的实时3d游戏中常用的软件变换与光照而言的。因此，最早的显卡最主要的目标也就是通过硬件来加速顶点变换，和顶点光照（如果严格来说，纹理采样应该也要算在内），从DX的固定管线接口中可以看出这一点，在固定管线中，通过SetTransform来设置变换矩阵，通过SetMaterial来设置材质属性，SetLight来设置光源参数，最后一个DrawPirmitive把三角形画到表面上去。 这几个步骤分别都是什么意思？接下来我会一一分说。 对于3D图形学有所了解的同学应当知道，绘制3D画面时，我们需要几样东西： 1) 所要绘制的目标：一个Mesh网格（包含了若干顶点，若干索引，实际构成为一个个三角形） 2) 物体的世界变换矩阵：通常我们表示一个3d物体（比如一个Mesh）的时候，都是用Local坐标系进行表示，实际渲染时，再乘上变换矩阵得到其在世界中的位置。这种做法有什么好处？或者是否非如此不可？读者可以想一想，其实原因非常简单。 : ) 3) 摄像机与视口：我们如何去看待三维世界中的物体，摄像机定义了我们观察物体的位置，角度，以及远近变换的程度 上述三样东西是必须的，有了相机和绘制目标，以及绘制目标的世界矩阵，我们就可以进行3D绘制了。但是想要获得更加具有真实感的图像，我们需要给绘制目标加上光照（注意，光照不等同于阴影，尽管当我们要实施阴影的时候，也往往是按照光照时设置的光源来绘制阴影的）此处所说的光照是指顶点光照，根据光源与顶点的相对位置，顶点的法线朝向，以及物体的受光材质决定最终的顶点颜色。一个简单的光照函数类似下面这样： DiffuseColor = Clamp(DotProduct(VertexNormal, VertexToLightDir), 0, 1) * LightColor * MaterialColor + Ambient Clamp函数保证点积的结果在[0.0, 1.0]区间内 上面的这个函数没有考虑光源衰减，灯光范围，不考虑SpotLight的衰减，另外只涉及漫反射光照，高光则需另外计算。但是从原理上已经可以说明（顶点）光照是怎么一回事了。这里强调顶点光照，是相对于像素光照来说的，顶点光照与像素光照后文会详述。 公式中提到的VertexToLightDir, LightColor属于光源的属性，在D3D9中则是由D3DLIGHT9来描述的，通过D3DDevice::SetLight接口进行设置。而MaterialColor属于材质属性，描述结构为D3DMATERIAL9，通过SetMaterial设置。 这样我们得到了绘制3D物体的另两样参数，分别为： 4) 光源：决定灯光的强度，颜色，范围，衰减等 5) [...]]]></description>
			<content:encoded><![CDATA[<p>初接触d3d时，相信许多初学者和我一样，虽然对3D实时渲染的原理有所了解，但是却对整体的管线结构有所困惑，包括可能许多做3d游戏的程序员，由于常常采用封装好的引擎做上层逻辑开发，因此对底层的架构也未必了解得十分透彻。</p>
<p>长久以来，对3d图形学的底层技术，我一直也是只了解个只鳞半爪而已，最近一段时间，在工作中不断的接触到这方面的知识，才有了比较系统的思考与总结，本文记叙了我学习dx管线的一些思考，由于水平所限以及尚未对最新的DX11架构有所关注，所以本文不涵盖DX11的最新架构，只涉及DX9及以前的知识结构，虽然如此，但是相信如果系统的掌握了这块知识，对了解后续的新技术也是有所帮助的。</p>
<p>本文主要是针对对3D渲染知识有一定了解，然而对D3D的结构却不是很清楚的同学，把DX8/9的管线结构做了一番说明，将固定管线的光照、纹理混合与可编程管线加以对比，期望对这一块知识有一个比较完整的总结。</p>
<p>&nbsp;</p>
<h5>目录</h5>
<p>1. 硬件加速的3D渲染</p>
<p>2. 材质与光照</p>
<p>3. 变换，光照与顶点着色器</p>
<p>4. 纹理与像素着色器</p>
<p>5. Alpha测试，深度测试与Alpha混合</p>
<p>6. 更多话题</p>
<p><span id="more-231"></span></p>
<h5>1. 硬件加速的3D渲染</h5>
<p>硬件加速的3D渲染，早些时候只是被称为Hardware Transform &amp; Lighting的加速技术，这些技术是相对于早期时候，图形硬件尚未普及，软件实现的实时3d游戏中常用的软件变换与光照而言的。因此，最早的显卡最主要的目标也就是通过硬件来加速顶点变换，和顶点光照（如果严格来说，纹理采样应该也要算在内），从DX的固定管线接口中可以看出这一点，在固定管线中，通过SetTransform来设置变换矩阵，通过SetMaterial来设置材质属性，SetLight来设置光源参数，最后一个DrawPirmitive把三角形画到表面上去。</p>
<p>这几个步骤分别都是什么意思？接下来我会一一分说。</p>
<p>对于3D图形学有所了解的同学应当知道，绘制3D画面时，我们需要几样东西：</p>
<p>1) 所要绘制的目标：一个Mesh网格（包含了若干顶点，若干索引，实际构成为一个个三角形）</p>
<p>2) 物体的世界变换矩阵：通常我们表示一个3d物体（比如一个Mesh）的时候，都是用Local坐标系进行表示，实际渲染时，再乘上变换矩阵得到其在世界中的位置。这种做法有什么好处？或者是否非如此不可？读者可以想一想，其实原因非常简单。 : )</p>
<p>3) 摄像机与视口：我们如何去看待三维世界中的物体，摄像机定义了我们观察物体的位置，角度，以及远近变换的程度</p>
<p>上述三样东西是必须的，有了相机和绘制目标，以及绘制目标的世界矩阵，我们就可以进行3D绘制了。但是想要获得更加具有真实感的图像，我们需要给绘制目标加上光照（注意，光照不等同于阴影，尽管当我们要实施阴影的时候，也往往是按照光照时设置的光源来绘制阴影的）此处所说的光照是指顶点光照，根据光源与顶点的相对位置，顶点的法线朝向，以及物体的受光材质决定最终的顶点颜色。一个简单的光照函数类似下面这样：</p>
<p>DiffuseColor = Clamp(DotProduct(VertexNormal, VertexToLightDir), 0, 1) * LightColor * MaterialColor + Ambient</p>
<p><em>Clamp函数保证点积的结果在[0.0, 1.0]区间内</em></p>
<p>上面的这个函数没有考虑光源衰减，灯光范围，不考虑SpotLight的衰减，另外只涉及漫反射光照，高光则需另外计算。但是从原理上已经可以说明（顶点）光照是怎么一回事了。这里强调顶点光照，是相对于像素光照来说的，顶点光照与像素光照后文会详述。</p>
<p>公式中提到的VertexToLightDir, LightColor属于光源的属性，在D3D9中则是由D3DLIGHT9来描述的，通过D3DDevice::SetLight接口进行设置。而MaterialColor属于材质属性，描述结构为D3DMATERIAL9，通过SetMaterial设置。</p>
<p>这样我们得到了绘制3D物体的另两样参数，分别为：</p>
<p>4) 光源：决定灯光的强度，颜色，范围，衰减等</p>
<p>5) 材质：决定物体受光的颜色，比如是绿色的物体，或红色的物体等。</p>
<p>有了光源和材质，绘制的3D物体则有了明暗和颜色，这对于提升渲染的真实感是有很大帮助的。</p>
<p>在此做一下小节：我们刚才回顾了绘制3D画面时，所必要做的事情，我们想象一下，如果没有3D硬件，或者不利用D3D，要实现上述功能，得由我们自己去完成哪些事？</p>
<p>首先要做软件的顶点变换，对每一个顶点，使用世界矩阵把它乘到世界坐标系中，用相机参数做必要的可见性剔除，利用相机朝向与面法线朝向做隐藏面消除，用光源与材质参数对世界空间中的顶点位置做顶点光照，将世界空间中的三角形变换到屏幕空间中，利用光照得到的顶点颜色，对三角形做<a href="http://en.wikipedia.org/wiki/Gouraud_shading">Gouraud插值着色</a>，最终得到绘制在屏幕上的图像。（有一本书叫做<a href="http://book.douban.com/subject/1321769/">《3D游戏编程大师技巧》</a>，对上述过程作了非常详尽的描述）</p>
<p>好了，有了图形硬件与D3D之后，上述工作都省掉了，我们通过SetTransform来设置世界矩阵、观察矩阵和投影矩阵，通过SetMaterial/SetLight来设置材质和光源参数，然后通过DrawPrimitive来绘制三角面。图形硬件和D3D接口极大的简化了我们编写3D程序的难度，诸多繁杂的事情都被交给硬件去完成了。</p>
<p>&nbsp;</p>
<h5>2. 材质与光照</h5>
<p>前一节主要是简单介绍了一下D3D硬件都为我们做了哪些事——事实上现如今的图形硬件所完成的功能已经远远不止上述那些了。接下来一节，我准备总结一下材质与光照的细节和原理。</p>
<p>上一节简单介绍了下材质与光照，但是没有深入系统的做总结，本节准备将这个过程做一个比较系统的总结。</p>
<p>关于几种基本光源，其公式，原理，实现，可以通过<a href="http://www.cnblogs.com/miloyip/archive/2010/04/02/1702768.html">MiloYip大牛的这篇文章</a>来了解。该文中的范例是基于光线追踪渲染器实现的，因此光和影的效果直接都有了，甚至在最后一个Demo中运用多光源模拟了软阴影的绘制。然而在光栅化渲染器中，光照与材质只能解决物体的颜色以及明暗问题，不能解决影子问题。要在光栅化渲染器中实现阴影的绘制，则是另外一番话题了。</p>
<p>接下来还是借助D3D的接口来说材质。</p>
<div class="codearea linewrap">
<pre><span style="color: blue;">typedef struct </span><span style="color: #010001;">_D3DMATERIAL9 </span>{
    <span style="color: #010001;">D3DCOLORVALUE   Diffuse</span>;        <span style="color: green;">/* Diffuse color RGBA */
    </span><span style="color: #010001;">D3DCOLORVALUE   Ambient</span>;        <span style="color: green;">/* Ambient color RGB */
    </span><span style="color: #010001;">D3DCOLORVALUE   Specular</span>;       <span style="color: green;">/* Specular 'shininess' */
    </span><span style="color: #010001;">D3DCOLORVALUE   Emissive</span>;       <span style="color: green;">/* Emissive color RGB */
    </span><span style="color: blue;">float           </span><span style="color: #010001;">Power</span>;          <span style="color: green;">/* Sharpness if specular highlight */
</span>} <span style="color: #010001;">D3DMATERIAL9</span>;</pre>
</div>
<p>材质描述了物体是如何对光作出响应的。虽然在可编程管线中，材质、光照都有自定义的实现方法，然而在固定管线中，光照采用的都是D3D所固有的一套机制，D3D中的最终光照结果由下式构成：</p>
<p>Global Illumination = Ambient Light + Diffuse Light + Specular Light + Emissive Light</p>
<p>环境光（Ambient Light）的计算方法为：Ambient Lighting = <a href="http://msdn.microsoft.com/en-us/library/bb172256(VS.85).aspx"><img src="http://latex.codecogs.com/gif.latex?C_{a}%20*%20[G_{a}%20+%20\sum%20\left%20(%20Atten_{i}%20*%20Spot_{i}%20*%20L_{ai}%20\right%20)]" alt="" /></a></p>
<p>其中C<sub>a</sub>为材质的Ambient分量，G<sub>a</sub>为全局环境光颜色（通过ID3DDevice9::SetRenderState(D3DRS_AMBIENT, COLOR)设置全局环境光颜色），求和的部分是所有被激活的光源的环境光进行求和，光源的衰减，聚光灯的Factor（如果是聚光灯）会被考虑在内。</p>
<p>漫反射光（Diffuse Light）的计算方法：Diffuse Lighting = <a href="http://msdn.microsoft.com/en-us/library/bb219656(VS.85).aspx"><img src="http://latex.codecogs.com/gif.latex?\sum%20(C_{d}%20*%20L_{d}%20*%20(N%20\cdot%20L_{dir}))%20*%20Atten%20*%20Spot)" alt="" /></a></p>
<p>解释一下这个方程，其中C<sub>d</sub>是材质的DiffuseColor，L<sub>d</sub>是光的DiffuseColor，N是顶点法线，L<sub>dir</sub>是从顶点到光源的方向向量（N与L<sub>dir</sub>都是单位向量），再乘以光源的衰减参数，及SpotLight的因子。这里的N<sup>.</sup>L<sub>dir</sub>正是计算漫反射光强弱的关键，一个顶点是否能被光源照亮，取决于该点法线与光方向的夹角，如果两者重合，该点受光程度达到最大，如果夹角大于等于90度，则不受光。</p>
<p>高光（Specular Light）的计算方法：Specular Lighting = <a href="http://msdn.microsoft.com/en-us/library/bb147399(VS.85).aspx"><img src="http://latex.codecogs.com/gif.latex?C_{s}%20*%20\sum%20[L_{s}%20*%20(N%20\cdot%20H)^{p}%20*%20Atten%20*%20Spot]" alt="" /></a></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>C<sub>s</sub>是材质的Specular值，p是材质中的Power，L<sub>s</sub>是灯光的Specular值，N是顶点法线，H是一个被称为Halfway vector的单位方向，该向量的含义为顶点到摄像机的向量与顶点到光源的向量的中间向量，计算方式为（H = norm(norm(C<sub>p</sub> &#8211; V<sub>p</sub>) + L<sub>dir</sub>)）</p>
<p>关于Halfway vector的含义，可以参考下图：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/09/halfwayvector.png"><img style="display: inline; border-width: 0px;" title="halfwayvector" src="http://www.windameister.org/blog/wp-content/uploads/2010/09/halfwayvector_thumb.png" border="0" alt="halfwayvector" width="244" height="244" /></a></p>
<p>仔细思考一下我们就能明白，相比将入射光向各个方向均匀反射的漫反射而言（对方向不敏感），高光对视线与反射光线方向是敏感的，也就是说，从眼睛到顶点与反射光方向的夹角越小就会越亮，日常生活中我们往往可以在光滑表面看到高光现象，比如汽车表面的喷漆或者玻璃。</p>
<p>最后一个是自发光，自发光很简单，就直接等于材质参数中的Emissive分量：Emissive Lighting = C<sub>e</sub></p>
<p>上述这些就是D3D固定管线中材质与光照的工作原理。其实所有的公式都可以在DXSDK的文档中获取到，这里不过是简单总结了一下。</p>
<p>前面所描述的这些材质与光照，依赖于正确的输入，比如顶点法线：漫反射和高光的计算都要依赖于顶点法线。我们都知道D3D有自定义的顶点格式，被称为FVF的东西。那么也就是说，如果想要正确的使用D3D顶点光照，我们采用的顶点格式，应当至少包含法线（D3DFVF_NORMAL）。</p>
<p>我们也常见到不包含法线，而是包含漫反射颜色（D3DFVF_DIFFUSE）和高光色彩（D3DFVF_SPECULAR）的顶点格式，采用这种顶点格式往往意味着我们准备自行为顶点填充颜色，而不采用D3D光照。</p>
<p>&nbsp;</p>
<h5>3. 变换，光照与顶点着色器</h5>
<p>要说顶点着色器，则不得不从固定管线与顶点格式说起。D3D中有所谓的Flexible Vertex Format，也就是大名鼎鼎的FVF常量。我们常见的D3DFVF_DIFFUSE，D3DFVF_XYZ，D3DFVF_NORMAL都是定义好的常量，在需要用的时候，我们把这些常量“或”在一起，构成一个顶点格式，传递给D3D使用。D3D固定管线通过该FVF格式对我们传入的VertexBuffer进行解释（每份顶点数据的长度，以及每个成员的偏移）。</p>
<p>起初学习D3D的时候，我对FVF格式有个疑惑，百思而不得其解。我们都知道要写一个普通的具有位置，法线和UV的顶点格式声明应该怎么写：</p>
<div class="codearea linewrap">
<pre><span style="color: blue;">#define </span><span style="color: #010001;">MY_VERTEX_FVF D3DFVF_XYZ </span>| <span style="color: #010001;">D3DFVF_NORMAL </span>| <span style="color: #010001;">D3DFVF_TEX0

</span><span style="color: blue;">struct </span><span style="color: #010001;">VERTEX
</span>{
    <span style="color: blue;">float </span><span style="color: #010001;">x</span>, <span style="color: #010001;">y</span>, <span style="color: #010001;">z</span>;
    <span style="color: blue;">float </span><span style="color: #010001;">nx</span>, <span style="color: #010001;">ny</span>, <span style="color: #010001;">nz</span>;
    <span style="color: blue;">float </span><span style="color: #010001;">u</span>, <span style="color: #010001;">v</span>;
};</pre>
</div>
<p>我觉得很奇怪，D3D怎么知道我把x，y，z放在前面还是把nx，ny，nz放在前面，难道是根据我定义FVF时的先后顺序？明显讲不通啊：因为或运算不可能给出任何先后顺序的信息来的。</p>
<p>直到后来我才明白，原来D3D固定管线的顶点格式也是有固定顺序的，也就是说如果要写一个既带有位置，又带有法线的顶点数据结构，那么x，y，z一定要放在nx，ny，nz之前，或者说，D3D是根据一个固定的偏移去解释这个顶点数据结构的，首先一定是位置，然后才是法线，其他更复杂包含更多成员的数据格式也是与此同理。什么意思呢？</p>
<p>还是以上面的例子来解释，你把顶点数据结构写成下面这样（区别在于，nx，ny，nz与x，y，z调换了顺序）：</p>
<div class="codearea linewrap">
<pre><span style="color: blue;">#define </span><span style="color: #010001;">MY_VERTEX_FVF D3DFVF_XYZ </span>| <span style="color: #010001;">D3DFVF_NORMAL </span>| <span style="color: #010001;">D3DFVF_TEX0

</span><span style="color: blue;">struct </span><span style="color: #010001;">VERTEX
</span>{
    <span style="color: blue;">float </span><span style="color: #010001;">nx</span>, <span style="color: #010001;">ny</span>, <span style="color: #010001;">nz</span>;
    <span style="color: blue;">float </span><span style="color: #010001;">x</span>, <span style="color: #010001;">y</span>, <span style="color: #010001;">z</span>;
    <span style="color: blue;">float </span><span style="color: #010001;">u</span>, <span style="color: #010001;">v</span>;
};</pre>
</div>
<p>D3D会说，不好意思，我不知道你怎么命名的，也不在乎你把它叫nx还是x，反正我都把你你头三个浮点数就当作是位置，接下来三个浮点数当成是法线，只要你给我的FVF是有D3DFVF_XYZ的，那头三个浮点就是位置没跑了！</p>
<p>从上面的例子中我们可以看到，实际上固定管线的结构是很死板的，缺乏给程序员自由发挥的空间，它规定好了一系列的顶点数据的用途，然后你必须按照它指定的方式去做，否则结果就不对。</p>
<p>当然了，这也只是后来的可编程管线替代固定管线的诸多原因之一。如今我们见到的市面上的所有显卡基本上已经没有不支持可编程管线的了，而且DX也已经更新换代到了DX11，新架构与本文中所介绍的基于DX9的结构又有了很多变化。</p>
<p>继续前面所述的话题，D3DFVF格式不仅决定了顶点数据结构如何定义，也说明了顶点要如何进行坐标系变换，比如：</p>
<p>D3DFVF_XYZ说明了顶点需要进行全套的世界、观察、投影变换；</p>
<p>D3DFVF_XYZRHW则说明该顶点坐标已经是屏幕坐标，x，y表示窗口中的坐标，z值表示z-buffer中的值，从近到远为0.0~1.0，采用这种格式的顶点声明不能与D3DFVF_XYZ或者D3DFVF_NORMAL混用，并且不经过顶点处理单元；</p>
<p>D3DFVF_XYZW也是表示已经变换过后的顶点，但是这种顶点格式会经过顶点处理单元，也就是vertexshader的阶段；</p>
<p>D3DFVF_XYZBn (n=1..4) 表示该顶点在变换阶段需要进行混合，啥意思？也就是说在做变换的时候，不是一个世界矩阵对其起作用，而是有n个矩阵，根据Blend的参数共同对该顶点起作用。如下例（摘自DXSDK）：</p>
<div class="codearea linewrap">
<pre><span style="color: blue;">#define </span><span style="color: #010001;">D3DFVF_BLENDVERTEX </span>(<span style="color: #010001;">D3DFVF_XYZB3</span>|<span style="color: #010001;">D3DFVF_NORMAL</span>|<span style="color: #010001;">D3DFVF_TEX1</span>)

<span style="color: blue;">struct </span><span style="color: #010001;">BLENDVERTEX
</span>{
    <span style="color: #010001;">D3DXVECTOR3 v</span>;       <span style="color: green;">// Referenced as v0 in the vertex shader
    </span><span style="color: #010001;">FLOAT       blend1</span>;  <span style="color: green;">// Referenced as v1.x in the vertex shader
    </span><span style="color: #010001;">FLOAT       blend2</span>;  <span style="color: green;">// Referenced as v1.y in the vertex shader
    </span><span style="color: #010001;">FLOAT       blend3</span>;  <span style="color: green;">// Referenced as v1.z in the vertex shader
    // v1.w = 1.0 - (v1.x + v1.y + v1.z)
    </span><span style="color: #010001;">D3DXVECTOR3 n</span>;       <span style="color: green;">// Referenced as v3 in the vertex shader
    </span><span style="color: #010001;">FLOAT       tu</span>, <span style="color: #010001;">tv</span>;  <span style="color: green;">// Referenced as v7 in the vertex shader
</span>};</pre>
</div>
<p>也就是说v在被变换的时候，会被4个矩阵依次乘上，并将其结果使用（blend1,blend2,blend3,1-(blend1 + blend2 + blend3)）四个参数作为系数进行混合。注意一下SetTransform函数，它有两个参数，第一个参数的说明里有这么一段：</p>
<p>&nbsp;</p>
<dt><em>State</em>&nbsp;</p>
</dt>
<dd>[in] Device-state variable that is being modified. This parameter can be any member of the <a href="http://msdn.microsoft.com/en-us/library/bb172619(VS.85).aspx">D3DTRANSFORMSTATETYPE</a> enumerated type, or the <a href="http://msdn.microsoft.com/en-us/library/bb172623(VS.85).aspx">D3DTS_WORLDMATRIX</a> macro.</dd>
<dd>其中的D3DTS_WORLDMATRIX是一个宏，可以像这么用D3DTS_WORLDMATRIX(0)，D3DTS_WORLDMATRIX(1)，这样一来就可以传递多个矩阵到固定管线中了（等到可编程管线就没这么麻烦了，可以直接通过顶点着色器常量传递矩阵）。</p>
<p>最后简单说一下D3DFVF_XYZBn这种顶点格式的用途：这种顶点格式往往被用在骨骼动画的顶点里，因为骨骼动画需要一个顶点受多个骨骼的影响。</p>
<p>定义好了FVF与顶点数据结构之后，我们需要把它设置到设备上去。我们知道，早些年的DX8也好，DX9也好，其Device都有一个SetVertexShader接口，用于设置顶点着色器程序。而在DX8中，这个接口不仅仅是可编程管线的vertex shader通过它来设置，实际上如果采用的是固定管线的话，顶点的FVF格式也是走这个接口设置的。DX8中的Vertex shader创建出来之后是一个32位的DWORD类型的handle，从而得以与FVF常量共用这同一个接口。而在DX9中，SetVertexShader的接受参数变成一个<strong>IDirect3DVertexShader9*</strong>了，而固定管线的FVF被挪走，需要通过接口SetFVF来进行设置了。</p>
<p>另外，由于在DX9中，<strong>IDirect3DVertexShader9</strong>在创建的时候，并没有包含顶点声明信息，因此DX9增加了一个接口<strong>IDirect3DVertexDeclaration9</strong>专门用于为可编程管线的VertexShader提供顶点声明信息。为什么一定要有顶点声明信息呢？写过HLSL的同学都知道，我们在vertex shader里是必须要定义vs input的数据结构的，顶点声明信息必须与我们的VS_INPUT数据结构保持一致。那么这个顶点声明信息在什么时候用呢？仔细思考一下就会明白，vs是一段运行于GPU上的程序，它按照我们编写的HLSL代码接受输入，经过计算后输出，输入的数据是在顶点缓冲中准备好的，在DrawPrimitive之前，通过SetVertexBuffer设置，此后这些输入数据通过驱动程序被提供给显卡，然而底层如何知晓应把多长一截的数据作为一份输入的顶点数据呢？以及应该把顶点数据中的哪一部分送到顶点寄存器里，哪些送到纹理采样单元里去？到这里，就不得不依赖顶点声明提供的信息了。</p>
<p>前面在材质与光照一节，我们简单介绍了一下D3D固定管线光照中，材质是如何与灯光一起起作用，并作用于物体的色泽，明暗，高光的。现在当我们手握Shader这样的利器，就拥有了绕开D3D固定管线光照的自由了（譬如如今已经很普及的基于像素的光照技术，就是把光照从顶点着色阶段推迟至像素着色器中）。我们还可以利用顶点着色器的常量传递变换矩阵，从而拥有自定义顶点变换，或者在顶点变换阶段获取一些我们想要的中间结果的能力。</p>
<h5>4. 纹理与像素着色器</h5>
<p>前面说了关于顶点变换与顶点光照相关的东西。接下来要说的是纹理混合，也就是SetTextureStageState函数干的事情。我们都知道DX支持纹理混合，我们可以把多个纹理按照Sampler的索引(DX9中是Sampler，而在DX8中则是Stage)，通过SetTexture函数设置到设备上。</p>
<p>&nbsp;</p>
<p>所谓Stage是什么一个概念呢？参考下图（来源于DX9SDK）：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/09/dx_texture_stage.png"><img style="display: inline; border-width: 0px;" title="dx_texture_stage" src="http://www.windameister.org/blog/wp-content/uploads/2010/09/dx_texture_stage_thumb.png" border="0" alt="dx_texture_stage" width="400" height="393" /></a></p>
<p>在每一个Stage上设有一张纹理（如果没有设置纹理），在固定管线中，我们通过<strong>SetTextureStageState</strong>设置在当前的Stage中，纹理如何与“当前像素”混合并输出到下一个Stage，“当前像素”来源于上一个Stage的输出，0Stage的当前像素的Color及Alpha是顶点Color/Alpha插值得来的。在纹理阶段比较常见的就是DiffuseTexture，SpecularTexture，分别用于颜色与高光。在每一个Stage中，纹理颜色与上一阶段颜色采用下式进行混合：</p>
</dd>
<p><em>FinalColor = TexelColor × SourceBlendFactor + PixelColor × DestBlendFactor</em></p>
<p>这里需要注意的是，纹理混合与最终的AlphaBlending是两个概念。纹理混合阶段，根据顶点颜色以及纹理决定最终要输出的像素的颜色和透明度，而到了AlphaBlending阶段，则是决定该像素与已经绘制到BackBuffer上的像素如何去混合。此外，更复杂的功能如NormalMapping，通常则需要像素着色器参与。按照我的理解，如果想要实现像素光照，则必须把光照阶段从顶点着色器中推迟到像素着色器中才可行，NormalMap提供了这一阶段每个像素的法线，从而使像素光照得以实现。（D3D9在固定管线中提供了一套<a href="http://msdn.microsoft.com/en-us/library/bb206304(VS.85).aspx">BumpMap</a>，但是老实说，<a href="http://msdn.microsoft.com/en-us/library/bb172379(VS.85).aspx">D3D9文档里对此的说明</a>看得我云里雾里的，并没有搞清楚其原理究竟是什么。）</p>
<p>像素着色器最初的就是为了替代固定管线的纹理混合操作而诞生的。也就是说，如果采用了PixelShader对像素进行着色，那么SetTextureStageState里面的那些颜色，Alpha混合操作就不再起作用了。</p>
<p>我们以一个常见的例子来解释纹理混合的用法，美术可能要求实现一种<a href="http://en.wikipedia.org/wiki/Blend_modes#Multiply">正片叠底</a>的纹理混合效果，对于程序实现而言，实现方案就是找两张贴图，用乘法进行纹理混合。如果采用SetTextureStageState实现：</p>
<p class="codearea linewrap">&nbsp;</p>
<pre><span style="color: #010001;">pD3DDevice</span>-&gt;<span style="color: #010001;">SetTextureStageState</span>(0, <span style="color: #010001;">D3DTSS_COLOROP</span>, <span style="color: #010001;">D3DTOP_SELECTARG1</span>);
<span style="color: #010001;">pD3DDevice</span>-&gt;<span style="color: #010001;">SetTextureStageState</span>(0, <span style="color: #010001;">D3DTSS_COLORARG1</span>, <span style="color: #010001;">D3DTA_TEXTURE</span>);

<span style="color: #010001;">pD3DDevice</span>-&gt;<span style="color: #010001;">SetTextureStageState</span>(1, <span style="color: #010001;">D3DTSS_COLOROP</span>, D<span style="color: #010001;">3DTOP_MODULATE</span>);
<span style="color: #010001;">pD3DDevice</span>-&gt;<span style="color: #010001;">SetTextureStageState</span>(1, <span style="color: #010001;">D3DTSS_COLORARG1</span>, <span style="color: #010001;">D3DTA_TEXTURE</span>);
<span style="color: #010001;">pD3DDevice</span>-&gt;<span style="color: #010001;">SetTextureStageState</span>(1, <span style="color: #010001;">D3DTSS_COLORARG2</span>, <span style="color: #010001;">D3DTA_CURRENT</span>);</pre>
<p>&nbsp;</p>
<p>解释一下这里的两个StageState的设置：阶段0，我们对Color采用SELECTARG1操作，同时把COLORARG1，设置为D3DTA_TEXTURE，也就是说，完全以SetTexture(0, …)设置的纹理颜色作为这一阶段的输出；在阶段1，COLOROP为MODULATE，也就是将COLORARG1与COLORARG2相乘作为输出，公式：<img src="http://latex.codecogs.com/gif.latex?S_{rgba}%20=%20Arg1%20\times%20Arg2" alt="" /></p>
<p>&nbsp;</p>
<p>其中COLORARG1设置为D3DTA_TEXTURE，也就是SetTexture(1, …)中设置的纹理，而COLORARG2为D3DTA_CURRENT，表示的是上一阶段的输出。这样经过这两个Stage之后，我们获得的输出就是两张贴图相乘的结果了。</p>
<p>接下来再用Ps2.0（需要DX9，如果采用DX8则需用ps1.4实现）实现一遍该效果：</p>
<p class="codearea">&nbsp;</p>
<pre><span style="color: #010001;">texture g_Tex0</span>;
<span style="color: #010001;">texture g_Tex1</span>;
<span style="color: #010001;">sampler g_Sampler0 </span>= <span style="color: #010001;">sampler_state </span>{<span style="color: #010001;">Texture </span>= <span style="color: #010001;">g_Tex0</span>; <span style="color: #010001;">MipFilter </span>= <span style="color: #010001;">LINEAR</span>; <span style="color: #010001;">MinFilter </span>= <span style="color: #010001;">LINEAR</span>; <span style="color: #010001;">MagFilter </span>= <span style="color: #010001;">LINEAR</span>;}
<span style="color: #010001;">sampler g_Sampler1 </span>= <span style="color: #010001;">sampler_state </span>{<span style="color: #010001;">Texture </span>= <span style="color: #010001;">g_Tex1</span>; <span style="color: #010001;">MipFilter </span>= <span style="color: #010001;">LINEAR</span>; <span style="color: #010001;">MinFilter </span>= <span style="color: #010001;">LINEAR</span>; <span style="color: #010001;">MagFilter </span>= <span style="color: #010001;">LINEAR</span>;}

<span style="color: blue;">struct </span><span style="color: #010001;">VS_OUTPUT
</span>{
    <span style="color: #010001;">float4 Position   </span>: <span style="color: #010001;">POSITION</span>;   <span style="color: green;">// vertex position
    </span><span style="color: #010001;">float2 TexCoord   </span>: <span style="color: #010001;">TEXCOORD0</span>;  <span style="color: green;">// vertex texture coords
</span>};

<span style="color: #010001;">float4 PS_Main_2_0</span>( <span style="color: #010001;">VS_OUTPUT Input </span>) : <span style="color: #010001;">COLOR0
</span>{
    <span style="color: #010001;">float4 color0 </span>= <span style="color: #010001;">tex2D</span>( <span style="color: #010001;">g_Sampler0</span>, <span style="color: #010001;">Input</span>.<span style="color: #010001;">TexCoord </span>);
    <span style="color: #010001;">float4 color1 </span>= <span style="color: #010001;">tex2D</span>( <span style="color: #010001;">g_Sampler1</span>, <span style="color: #010001;">Input</span>.<span style="color: #010001;">TexCoord </span>);
    <span style="color: blue;">return </span><span style="color: #010001;">float4</span>(<span style="color: #010001;">color0 </span>* <span style="color: #010001;">color1</span>);
}</pre>
<p>&nbsp;</p>
<p>使用ps的话，所需要做的工作非常直观，用预先定义好的<a href="http://msdn.microsoft.com/en-us/library/bb509644(VS.85).aspx">Sampler</a>以及uv坐标对纹理采样，并把颜色相乘输出即可。</p>
<p>这只是一个最简单的例子，实际上ps的用途非常广泛（实现ShaderMap，像素光照，诸多后期处理效果都需要采用ps实现），而更多的内容就不可能涵盖在本文的范围之内了，感兴趣的读者可以读一读GPUGems系列，ShaderX系列等经典书籍。</p>
<h5>5. Alpha测试，深度测试，以及Alpha混合</h5>
<p>走完了像素着色的过程之后，就到了最终的绘制步骤了。这里有几个因素决定我们是否，以及如何往BackBuffer/ZBuffer/StencilBuffer上绘制像素：AlphaTest，StencilTest，ZTest，Alpha混合模式。</p>
<p>关于上述测试的顺序和流程，由于OpenGl与D3D在上述测试的顺序上达成了一致，因此可以参考下图（图摘自<a href="http://www.opengl.org/documentation/specs/version2.0/glspec20.pdf">OpenGL2.0 spec Page 199</a>）：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/09/PreFragmentOperations.png"><img style="display: inline; border-width: 0px;" title="Pre-FragmentOperations" src="http://www.windameister.org/blog/wp-content/uploads/2010/09/PreFragmentOperations_thumb.png" border="0" alt="Pre-FragmentOperations" width="648" height="492" /></a></p>
<p>如果开启了AlphaTest，则首先做AlphaTest，根据像素的Alpha值与一个指定的D3DRS_ALPHAREF值比较的结果，将D3DRS_ALPHAFUNC的判断未能通过的像素直接砍掉。（不会进行后续的z-test以及z-write）</p>
<p>如果开启了StencilTest，则继续做StencilTest，如果不通过StencilTest，则也不会进行下一步ZTest，但是可能会修改StencilBuffer。</p>
<p>如果开启了ZTest，根据当前像素Z值，与DepthBuffer上该位置已有Z值做比较，如果D3DRS_ZFUNC的判断未能通过，则将该像素砍掉，但是根据D3DRS_STENCILZFAIL的状态，有可能会修改StencilBuffer。通过了ZBuffer的像素，如果开启了ZWrite，则该像素的Z值会写入DepthBuffer，否则（ZWrite关闭）该像素不会影响DepthBuffer（也就是说不会遮挡后续绘制的像素，尽管可能该像素离观察者比将来要绘制的像素更近）</p>
<p>最后根据D3DRS_ALPHABLENDENABLE决定是否开启Alpha混合。如果Alpha混合没有开启：则直接把像素的颜色透明度写入BackBuffer，如果Alpha混合开启了，则根据D3DRS_SRCBLEND以及D3DRS_DESTBLEND的选项，决定新写入像素如何与BackBuffer已有像素进行混合。</p>
<p>上述流程是标准流程，然而在较新的几代显卡里，针对StencilTest与ZTest又有了许多新的技术，比如EarlyZ优化等，由于这里牵扯到的显卡型号，各厂商可能都有不一致的地方，我就没有做更进一步的详细研究。下面有几个可以供参考的链接，感兴趣的同学可以自己了解一下：</p>
<p>amddeveloper的一篇文章在这里：<a href="http://developer.amd.com/media/gpu_assets/Depth_in-depth.pdf">http://developer.amd.com/media/gpu_assets/Depth_in-depth.pdf</a></p>
<p>beyond3d上的一个讨论帖看这里：<a href="http://forum.beyond3d.com/showthread.php?t=51025">http://forum.beyond3d.com/showthread.php?t=51025</a></p>
<p>&nbsp;</p>
<h5>6. 更多话题</h5>
<p>本文介绍了D3D流水线的一般过程，对比了固定管线与可编程管线的差别（当然有很多细节未能兼顾到），对D3D渲染流程的知识结构做了一个一般性的总结。</p>
<p>事实上，随着图形技术的发展，如今的3D图形技术已远走出很长一段路了，比如在DX9架构下就已经发展出很多成果的ShadowMap，NormalMap，Realtime Global Illumination等。再比如到DX10新增的Geometry Shader以及DX11架构下新增的Compute Shader，Tessellation等。</p>
<p>有时候学得越多，才越知道自己所知甚少。在撰写本文的过程中，我也把此前有些混淆的概念做了整理，自己也有不少收获。如果读者对本文所涉的话题有更多理解，或者发现了错漏之处，还望不吝告知。 : )</p>
<p>&nbsp;</p>
<p>#1 更正了文中的几处文字、公式错误，修改了针对高光概念的描述。(感谢<a href="http://1111h.blogspot.com/">Rainsing</a>的帮助)</p>
]]></content:encoded>
			<wfw:commentRss>http://www.windameister.org/blog/2010/09/18/d3dbasic-lighting-material-shading/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>溶解效果的原理与实现#1</title>
		<link>http://www.windameister.org/blog/2010/09/11/dissolve-effect-implement-in-dx9/</link>
		<comments>http://www.windameister.org/blog/2010/09/11/dissolve-effect-implement-in-dx9/#comments</comments>
		<pubDate>Sat, 11 Sep 2010 04:21:30 +0000</pubDate>
		<dc:creator>windam</dc:creator>
				<category><![CDATA[shader]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[3d]]></category>
		<category><![CDATA[dissolve]]></category>
		<category><![CDATA[effect]]></category>
		<category><![CDATA[pixel shader]]></category>

		<guid isPermaLink="false">http://www.windameister.org/blog/2010/09/11/%e6%ba%b6%e8%a7%a3%e6%95%88%e6%9e%9c%e7%9a%84%e5%8e%9f%e7%90%86%e4%b8%8e%e5%ae%9e%e7%8e%b0/</guid>
		<description><![CDATA[在游戏中实现溶解效果，往往有两种选择：其一是采用多重纹理；其二是采用模板缓冲。 本文说明采用多重纹理方案时如何实现溶解效果，采用模板缓冲的方案容后再补。 上图是一个利用PixelShader达成的效果，其原理是，当我们把一个面片绘制到渲染表面上时，将特定像素的Alpha值设为0，从而在绘制到表面与背景混合时，达到镂空的效果。镂空的图案由美术指定一张Mask纹理确定。譬如在上图的效果中，我利用了下面这样的两张纹理图（左图是前景贴图，右图作为Mask），美术可以通过指定不同的Mask图，实现不同类型的逐渐消解/生成的效果： 实现此效果需要两张贴图，一张是物体表面的纹理，另一张作为溶解的Mask图。 &#160; &#160; PixelShader(ps_2_0)代码如下： float g_DissolveThreshold;    // 该参数作为阈值，与mask图上的当前像素比较大小以决定像素是否镂空 float4 PS_Main_2_0( VS_OUTPUT Input ) : COLOR0 { float4 DiffuseColor = tex2D( diffuse_sampler, Input.TexCoord ); float4 TexDissolve = tex2D( mask_sampler, Input.TexCoord ); DiffuseColor.rgb = DiffuseColor.rgb * Input.Diffuse.rgb; float originalalpha = Input.Diffuse.a * DiffuseColor.a; float dissolveDelta = TexDissolve.r &#8211; g_DissolveThreshold; if (dissolveDelta &#62;= 0) DiffuseColor.a [...]]]></description>
			<content:encoded><![CDATA[<p>在游戏中实现溶解效果，往往有两种选择：其一是采用多重纹理；其二是采用模板缓冲。</p>
<p>本文说明采用多重纹理方案时如何实现溶解效果，采用模板缓冲的方案容后再补。</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/09/DissolveMiddle.jpg"><img style="display: inline; border-width: 0px;" title="Dissolve-Middle" src="http://www.windameister.org/blog/wp-content/uploads/2010/09/DissolveMiddle_thumb.jpg" border="0" alt="Dissolve-Middle" width="244" height="223" /></a></p>
<p>上图是一个利用PixelShader达成的效果，其原理是，当我们把一个面片绘制到渲染表面上时，将特定像素的Alpha值设为0，从而在绘制到表面与背景混合时，达到镂空的效果。镂空的图案由美术指定一张Mask纹理确定。譬如在上图的效果中，我利用了下面这样的两张纹理图（左图是前景贴图，右图作为Mask），美术可以通过指定不同的Mask图，实现不同类型的逐渐消解/生成的效果：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/09/06_11_2_moon.jpg"><img style="display: inline; border-width: 0px;" title="06_11_2_moon" src="http://www.windameister.org/blog/wp-content/uploads/2010/09/06_11_2_moon_thumb.jpg" border="0" alt="06_11_2_moon" width="231" height="244" /></a> <a href="http://www.windameister.org/blog/wp-content/uploads/2010/09/dissolve.jpg"><img style="display: inline; border-width: 0px;" title="dissolve" src="http://www.windameister.org/blog/wp-content/uploads/2010/09/dissolve_thumb.jpg" border="0" alt="dissolve" width="197" height="244" /></a></p>
<p><em>实现此效果需要两张贴图，一张是物体表面的纹理，另一张作为溶解的Mask图。</em></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><span id="more-220"></span></p>
<p>PixelShader(ps_2_0)代码如下：</p>
<p>float g_DissolveThreshold;    // 该参数作为阈值，与mask图上的当前像素比较大小以决定像素是否镂空</p>
<p>float4 PS_Main_2_0( VS_OUTPUT Input ) : COLOR0<br />
{<br />
float4 DiffuseColor = tex2D( diffuse_sampler, Input.TexCoord );<br />
float4 TexDissolve = tex2D( mask_sampler, Input.TexCoord );<br />
DiffuseColor.rgb = DiffuseColor.rgb * Input.Diffuse.rgb;<br />
float originalalpha = Input.Diffuse.a * DiffuseColor.a;<br />
float dissolveDelta = TexDissolve.r &#8211; g_DissolveThreshold;<br />
if (dissolveDelta &gt;= 0)<br />
DiffuseColor.a = originalalpha;<br />
else<br />
DiffuseColor.a = 0;<br />
return DiffuseColor;<br />
};</p>
<p>该函数的原理是这样的：我们在光栅化三角形的时候，通过pixelshader给当前像素着色，首先分别取得两张纹理当前颜色（通过前一阶段VS输出的uv坐标Input.TexCoord，以及Sampler进行采样，<a href="http://msdn.microsoft.com/en-us/library/bb509644(VS.85).aspx">这里的diffuse_sampler, mask_sampler是此前定义的分别针对两张纹理的Sampler</a>他们指定了针对哪个纹理采样，以及如何采样），对Mask纹理，我们只取r通道的值作为判断依据(<strong>TexDissolve.r</strong>)——通常情况下作为Mask纹理的图可以采用8bit单通道图以节省空间，采用r通道是为了简便，实际上对于灰度图而言，无论采用哪个通道都是相同的。如果传入常量g_DissolveThreshold大于TexDissolve.r，则输出像素的Alpha被置为0。</p>
<p>事实上，上述溶解效果只考虑以Alpha作为因子的Alpha混合（AlphaBlending以D3DBLEND_SRCALPHA作为混合的因子的情况），因此输出像素的Alpha被置为0即可。如果需要考虑其他的混合模式，例如D3DBLEND_SRCCOLOR，则需将输出像素整体置为0。这里涉及到Alpha混合相关的知识，我会另外撰文总结。</p>
<p>其中g_DissolveThreshold是一个Pixel Shader常量，程序逻辑中可以控制该常量的变化，而后将值传入ps，以达到不同程度的溶解。改变该变量的行为方式，则可以实现各种不同的溶解/生成效果。</p>
<p>p.s.1使用上述逻辑实现溶解效果时，可以根据g_DissolveThreshold与Mask像素差的大小，增加一些判定从而实现边缘描边，边缘淡出等效果。</p>
<p>p.s.2 如果想针对Mesh实现该效果，其原理与面片的类似，可以利用相似的办法（Mask贴图以及针对模型的PixelShader），实现模型或物体在背景中溶解消失的效果</p>
<p>p.s.3 本文中所用ps如果在dx8.1版本或之前，需要采用ps1.4，则应当利用cmp指令解决alpha值的运算问题：</p>
<p><em>; ps1.4</em></p>
<p><em>; c0 is g_DissolveThreshold</em></p>
<p><em>; c1 is (0, 0, 0, 0)</em></p>
<p><em>texld r0, t0 ; diffuse texture</em></p>
<p><em>texld r1, t1 ; mask texture</em></p>
<p><em>mul r0, r0, v0 ; diffuse texture color * vertex diffuse color</em></p>
<p><em>add r2, r1, –c0 ; sub to get the dissolve delta</em></p>
<p><em>cmp r0.a r2, r0, c1 ; cmp using r2’s value r2.a &gt;= 0? use what is there (r0.a) , otherwise use 0</em></p>
<p>#1 增加了ps1.4版本的ps。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.windameister.org/blog/2010/09/11/dissolve-effect-implement-in-dx9/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>简述游戏逻辑及编辑器的抽象</title>
		<link>http://www.windameister.org/blog/2010/05/16/a-simple-approach-to-logic-and-abstraction-for-online-game-editor/</link>
		<comments>http://www.windameister.org/blog/2010/05/16/a-simple-approach-to-logic-and-abstraction-for-online-game-editor/#comments</comments>
		<pubDate>Sun, 16 May 2010 05:25:18 +0000</pubDate>
		<dc:creator>windam</dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[网络游戏]]></category>
		<category><![CDATA[抽象]]></category>
		<category><![CDATA[编辑器]]></category>
		<category><![CDATA[设计]]></category>
		<category><![CDATA[逻辑]]></category>

		<guid isPermaLink="false">http://www.windameister.org/blog/2010/05/16/%e7%bd%91%e7%bb%9c%e6%b8%b8%e6%88%8f%e9%80%bb%e8%be%91%e5%8f%8a%e7%bc%96%e8%be%91%e5%99%a8%e7%9a%84%e8%ae%be%e8%ae%a1%e4%b8%8e%e6%8a%bd%e8%b1%a1/</guid>
		<description><![CDATA[最近一段时间以来，本人参与了公司下一代游戏编辑器的开发，从而有机会针对编辑器设计做一些简单的思考——如何设计更好的抽象，从而达到在客户端，服务端，以及游戏编辑器中复用尽可能多的代码？如何能够尽可能的缩短游戏设计师（策划）及美术设计师（3D/2D场景美术）的工作流程？市面上优秀的引擎往往都附带有所见即所得编辑器，这样的编辑器应当如何设计？网络游戏编辑器又有哪些可以从中借鉴和学习之处？ 将尚不是很成熟的思考结果总结成本文。本人水平所限，许多错漏之处难免考虑不周全，如果有同学对本文所述的问题有任何想法，亦或是有其他文章与此相关，欢迎一起交流。 客户端逻辑的复用 把游戏客户端进行拆解，可以将其看成一个拥有输入、输出及内部逻辑循环的系统。玩家通过鼠标键盘发送消息给客户端，服务器通过网络接口发送消息给客户端，然后客户端于每一帧把当前对应的表现绘制到屏幕上。 客户端输入包括：Windows消息（键盘，鼠标，windows的其他消息，快捷键，定时器，摇杆等），网络消息（通过服务器或者其他玩家发送来的消息）；输出包括：屏幕显示，网络消息（发送给服务器的响应或请求）。 事实上一个游戏编辑器也可以简单的看成类似于上述的系统，其输入包括：Windows消息（键盘，鼠标，windows的其他消息，菜单快捷键等，定时器等）；输出则包括了屏幕显示，正常情况下编辑器不需要接受或发出任何网络消息（这里指与服务器进行逻辑上的通信，而非指通过版本控制系统进行数据的同步管理）。 实际上编辑器与客户端程序实际上都拥有相似的输入接口(Windows消息)，只是同样的消息引发了不同的逻辑——在这里我们可以对输入接口做以抽象，并使用不同的实现来分别实现其逻辑，从而达到在编辑器中拥有热切换编辑状态和游戏状态逻辑的能力。（许多知名的游戏引擎都拥有类似上述的可在编辑器中切换编辑/游戏状态的功能，比如Crysis的SandBox编辑器，再如RunicGames的TorchLight编辑器等）。 具体来说，通过设计一个类似如下的接口（只考虑位于单个窗口中操作的情况）： class Operation { public: virtual bool OnLButtonDown(POINT pt, UINT uFlags) = 0; virtual bool OnLButtonUp(POINT pt, UINT uFlags) = 0; virtual bool OnMouseMove(POINT pt, UINT uFlags) = 0; virtual bool OnKeyDown(UINT nChar, UINT nRepCounts, UINT nFlags) = 0; }; （此处仅起示意作用，就不将全部的接口都列出了） 实际程序中，我们在消息处理线程中将收到的Windows消息交给该接口，该接口背后的实现是编辑器逻辑呢？（左键按下是在选取场景中的某个物体，拖动其位置等）还是客户端逻辑呢？（左键按下是在尝试点击某个NPC或者怪物，并引发下一步动作，攻击之？对话之？）我们的前端程序并不关心接口背后的实现逻辑为何种。这样我们就可以达到动态替换逻辑的目的——在编辑器中一键切换运行模式。 这样做（所见即所玩）有什么好处？好处很多——如果不能在编辑器中立即看到游戏效果，那么游戏编辑人员（国内通常为策划人员，国外的游戏开发者中颇多为关卡设计师）必须首先在编辑器中将各类物体，对象，事件安放好，然后导出数据，再在游戏客户端中找到对应场景，并一一测试在编辑器中设置的对象。这种做法相比于在编辑器中立时可见（所编辑即所玩）的设计，其缺陷在于增加了中间环节，增加了出错可能性，当出现了与预想结果不同的现象时，定位问题所在需要花费更多时间，从而降低了游戏开发效率。此外，由于游戏开发设计的需要，往往编辑器编辑的许多数据并不是游戏最终直接使用的数据，因此存在编辑器导出数据的环节，编辑器需要将编辑好的数据导出，客户端程序需要将导出后数据进行加载，导出和加载都可能存在错误（比如编辑器中新增了功能（版本增加，文件结构改变），客户端必须增加或修改对应代码才能支持，这些环节的出错，都会降低游戏开发效率）。 良好的设计架构，通过把逻辑封装在一套预定义好的接口背后，使得同一套客户端/服务端逻辑，可以被复用于编辑器中，也可以复用于客户端/服务端程序中（逻辑代码的复用存在两个级别——代码级与二进制级，如果没有平台差异，那么在合理的设计中，我们理应做到二进制级的复用——即通过一个设计好的二进制动态运行库交给不同的进程加载；某些逻辑可能只能在源码级进行复用——比如服务器端逻辑；当然倘若采用脚本语言编写逻辑，我们可以忽略这里的差别）。当我们切换编辑器模式，程序从编辑状态转入游戏状态时，接口背后的实现被替换为客户端逻辑，最终达到在编辑器中伪造出客户端操作及逻辑的目的。 除了获得所见即所玩的好处，这种将输入接口抽象的做法，还有一个额外的好处——录像重播功能。将游戏视为输入输出系统的设计，给了我们将所有的输入事件按照时间顺序记录（通过输入设备的鼠标键盘事件，系统/定时器消息，网络消息等）。如果将所有的输入事件，按照时间记录保存下来，后再按照时间顺序作为输入重新调用对应的接口，我们就获得了整个游戏过程的重演。星际争霸，以及魔兽争霸等游戏中都有录像功能，可以将游戏过程记录下来，并重新播放，应当是基于类似的技术。 更进一步，由于网络游戏中的绝大部分逻辑是运行在服务器上的，例如与NPC交互时有什么反应，NPC会说什么话，会提供什么可选任务，会提供什么服务，NPC会如何行动，怪物会如何行动，怪物被击杀主角获得什么奖励，主角有什么技能，可以做什么事情（移动，施放技能，击杀怪物，拾取物品等）。诸如此类的逻辑实际上并非客户端逻辑，而是服务器逻辑，如何在编辑器中模拟这些东西呢？ 服务端逻辑的复用 不同于单机游戏——游戏逻辑运行于本机上，网络游戏往往多数逻辑都运行在服务器上。这就造成游戏开发中的困扰——编辑好的场景，NPC，怪物无法立即看到实际状况，游戏设计师们往往并不知道该NPC是否一切正常。设计师需要将编辑器中制作的场景导出给服务器端程序，服务端可能需要重启一次服务器，加载这部分数据，然后设计者再从客户端启动游戏，连接服务器并测试新加入的内容。（这其中的流程包括，编辑器导出数据给服务端，服务端重启，服务端读取新数据，启动客户端并连接服务器：每一个步骤都可能存在其他因素引入的错误，降低开发效率；类似的道理，当出现表现出来的状况与预想不同时，游戏设计师难以定位问题所在）。 [...]]]></description>
			<content:encoded><![CDATA[<p>最近一段时间以来，本人参与了公司下一代游戏编辑器的开发，从而有机会针对编辑器设计做一些简单的思考——如何设计更好的抽象，从而达到在客户端，服务端，以及游戏编辑器中复用尽可能多的代码？如何能够尽可能的缩短游戏设计师（策划）及美术设计师（3D/2D场景美术）的工作流程？市面上优秀的引擎往往都附带有所见即所得编辑器，这样的编辑器应当如何设计？网络游戏编辑器又有哪些可以从中借鉴和学习之处？</p>
<p>将尚不是很成熟的思考结果总结成本文。本人水平所限，许多错漏之处难免考虑不周全，如果有同学对本文所述的问题有任何想法，亦或是有其他文章与此相关，欢迎一起交流。</p>
<p><strong> </strong></p>
<p><strong>客户端逻辑的复用</strong><strong> </strong></p>
<p>把游戏客户端进行拆解，可以将其看成一个拥有输入、输出及内部逻辑循环的系统。玩家通过鼠标键盘发送消息给客户端，服务器通过网络接口发送消息给客户端，然后客户端于每一帧把当前对应的表现绘制到屏幕上。</p>
<p>客户端输入包括：Windows消息（键盘，鼠标，windows的其他消息，快捷键，定时器，摇杆等），网络消息（通过服务器或者其他玩家发送来的消息）；输出包括：屏幕显示，网络消息（发送给服务器的响应或请求）。</p>
<p>事实上一个游戏编辑器也可以简单的看成类似于上述的系统，其输入包括：Windows消息（键盘，鼠标，windows的其他消息，菜单快捷键等，定时器等）；输出则包括了屏幕显示，正常情况下编辑器不需要接受或发出任何网络消息（这里指与服务器进行逻辑上的通信，而非指通过版本控制系统进行数据的同步管理）。</p>
<p>实际上编辑器与客户端程序实际上都拥有相似的输入接口(Windows消息)，只是同样的消息引发了不同的逻辑——在这里我们可以对输入接口做以抽象，并使用不同的实现来分别实现其逻辑，从而达到在编辑器中拥有热切换编辑状态和游戏状态逻辑的能力。（许多知名的游戏引擎都拥有类似上述的可在编辑器中切换编辑/游戏状态的功能，比如Crysis的SandBox编辑器，再如RunicGames的TorchLight编辑器等）。<span id="more-202"></span></p>
<p>具体来说，通过设计一个类似如下的接口（只考虑位于单个窗口中操作的情况）：</p>
<div class="codearea">
<pre><span style="color: blue;">class </span><span style="color: #010001;">Operation
</span>{
<span style="color: blue;">public</span>:
    <span style="color: blue;">virtual bool </span><span style="color: #010001;">OnLButtonDown</span>(<span style="color: #010001;">POINT pt</span>, <span style="color: #010001;">UINT uFlags</span>) = 0;
    <span style="color: blue;">virtual bool </span><span style="color: #010001;">OnLButtonUp</span>(<span style="color: #010001;">POINT pt</span>, <span style="color: #010001;">UINT uFlags</span>) = 0;
    <span style="color: blue;">virtual bool </span><span style="color: #010001;">OnMouseMove</span>(<span style="color: #010001;">POINT pt</span>, <span style="color: #010001;">UINT uFlags</span>) = 0;
    <span style="color: blue;">virtual bool </span><span style="color: #010001;">OnKeyDown</span>(<span style="color: #010001;">UINT nChar</span>, <span style="color: #010001;">UINT nRepCounts</span>, <span style="color: #010001;">UINT nFlags</span>) = 0;
};</pre>
<p><span style="color: #4e4e4e;"> </span></p>
<p>（此处仅起示意作用，就不将全部的接口都列出了）</p>
</div>
<p>实际程序中，我们在消息处理线程中将收到的Windows消息交给该接口，该接口背后的实现是编辑器逻辑呢？（左键按下是在选取场景中的某个物体，拖动其位置等）还是客户端逻辑呢？（左键按下是在尝试点击某个NPC或者怪物，并引发下一步动作，攻击之？对话之？）我们的前端程序并不关心接口背后的实现逻辑为何种。这样我们就可以达到动态替换逻辑的目的——在编辑器中一键切换运行模式。</p>
<p>这样做（所见即所玩）有什么好处？好处很多——如果不能在编辑器中立即看到游戏效果，那么游戏编辑人员（国内通常为策划人员，国外的游戏开发者中颇多为关卡设计师）必须首先在编辑器中将各类物体，对象，事件安放好，然后导出数据，再在游戏客户端中找到对应场景，并一一测试在编辑器中设置的对象。这种做法相比于在编辑器中立时可见（所编辑即所玩）的设计，其缺陷在于增加了中间环节，增加了出错可能性，当出现了与预想结果不同的现象时，定位问题所在需要花费更多时间，从而降低了游戏开发效率。此外，由于游戏开发设计的需要，往往编辑器编辑的许多数据并不是游戏最终直接使用的数据，因此存在编辑器导出数据的环节，编辑器需要将编辑好的数据导出，客户端程序需要将导出后数据进行加载，导出和加载都可能存在错误（比如编辑器中新增了功能（版本增加，文件结构改变），客户端必须增加或修改对应代码才能支持，这些环节的出错，都会降低游戏开发效率）。</p>
<p><strong>良好的设计架构，通过把逻辑封装在一套预定义好的接口背后，使得同一套客户端</strong><strong>/</strong><strong>服务端逻辑，可以被复用于编辑器中，也可以复用于客户端</strong><strong>/</strong><strong>服务端程序中</strong>（逻辑代码的复用存在两个级别——代码级与二进制级，如果没有平台差异，那么在合理的设计中，我们理应做到二进制级的复用——即通过一个设计好的二进制动态运行库交给不同的进程加载；某些逻辑可能只能在源码级进行复用——比如服务器端逻辑；当然倘若采用脚本语言编写逻辑，我们可以忽略这里的差别）。当我们切换编辑器模式，程序从编辑状态转入游戏状态时，接口背后的实现被替换为客户端逻辑，最终达到在编辑器中伪造出客户端操作及逻辑的目的。</p>
<p>除了获得所见即所玩的好处，这种将输入接口抽象的做法，还有一个额外的好处——录像重播功能。将游戏视为输入输出系统的设计，给了我们将所有的输入事件按照时间顺序记录（通过输入设备的鼠标键盘事件，系统/定时器消息，网络消息等）。如果将所有的输入事件，按照时间记录保存下来，后再按照时间顺序作为输入重新调用对应的接口，我们就获得了整个游戏过程的重演。星际争霸，以及魔兽争霸等游戏中都有录像功能，可以将游戏过程记录下来，并重新播放，应当是基于类似的技术。</p>
<p>更进一步，由于网络游戏中的绝大部分逻辑是运行在服务器上的，例如与NPC交互时有什么反应，NPC会说什么话，会提供什么可选任务，会提供什么服务，NPC会如何行动，怪物会如何行动，怪物被击杀主角获得什么奖励，主角有什么技能，可以做什么事情（移动，施放技能，击杀怪物，拾取物品等）。诸如此类的逻辑实际上并非客户端逻辑，而是服务器逻辑，如何在编辑器中模拟这些东西呢？</p>
<p><strong>服务端逻辑的复用</strong><strong></strong></p>
<p>不同于单机游戏——游戏逻辑运行于本机上，网络游戏往往多数逻辑都运行在服务器上。这就造成游戏开发中的困扰——编辑好的场景，NPC，怪物无法立即看到实际状况，游戏设计师们往往并不知道该NPC是否一切正常。设计师需要将编辑器中制作的场景导出给服务器端程序，服务端可能需要重启一次服务器，加载这部分数据，然后设计者再从客户端启动游戏，连接服务器并测试新加入的内容。（这其中的流程包括，编辑器导出数据给服务端，服务端重启，服务端读取新数据，启动客户端并连接服务器：每一个步骤都可能存在其他因素引入的错误，降低开发效率；类似的道理，当出现表现出来的状况与预想不同时，游戏设计师难以定位问题所在）。</p>
<p>由于本人没有编写过服务端代码，对网络游戏服务端的架构并不是很熟悉，因此以下的分析仅从个人理想化的角度出发，简单谈谈服务端逻辑在编辑器中的整合。服务端运行的游戏逻辑，主要包括玩家逻辑（客户端发送的让玩家做某某事的请求，其他玩家的状态广播，玩家状态改变，玩家数据改变），怪物逻辑（怪物的行为，技能，AI，掉落以及击杀奖励等），技能逻辑（技能的时间，伤害，状态等），可达地点判定（什么地方可以到达，什么地方不能走）。服务端逻辑也可以简单的看成一个大循环，一方面通过网络收到的消息和内部触发机制创造事件，另一方面不断的循环处理事件（期间可能要读数据库，读服务器数据等），并最终做以输出——通过网络发送消息给玩家，或者写数据库。</p>
<p>将服务端进行拆解：我们得到一个读取（从数据文件/网络/数据库），逻辑循环（处理事件），输出（写文件/写数据库/发送网络消息）的模型。其中数据文件——往往是由游戏设计师在场景中编辑的数据，或者技能数据，或者游戏里的其他模板（物品，人物，怪，NPC等）数据，数据库往往保存了玩家的数据，人物技能/人物等级/属性/物品等。</p>
<p>思考一下，我们发现，服务器逻辑所需要的游戏数据文件，各类数据模板，定时器，触发器等正是编辑器所拥有的数据。服务端所需要的网络访问，数据库访问，则可以通过<a href="http://en.wikipedia.org/wiki/Proxy_pattern">Proxy模式</a>伪造一个网络层和数据库层，让服务器逻辑运行在非真实服务器的环境下（虽然服务器逻辑会以为自己运行于服务器环境中）。这就给我们在编辑器环境中运行服务器逻辑提供了可能性。</p>
<p>最后，要想达到服务端代码复用，那么第一就是要求逻辑代码本身不具有任何的平台相关性——调用了LinuxAPI或者WindowsAPI都是应该避免的——理想化的做法是使用java/python之类的跨平台语言。（在同做服务端程序的同事的交流中，了解到服务端程序运行的瓶颈往往并不在于CPU计算，而在于网络IO上，因此采用非平台相关语言，未必会增加多少运行上的效率损失，反而带来了代码复用的便利性，换句话说，就算我们不采用平台无关语言编写服务端逻辑，比如采用c或者c++编写服务端逻辑，只要这部分逻辑代码不沾染任何平台相关API即可。）这么做的目的是为了服务端逻辑的跨平台复用。</p>
<p><strong>设计中尚存在的问题</strong><strong></strong></p>
<p>前面谈到的目标是好的，但事实上还存在许多尚且不确定的地方。接下来简单分析一下上述做法还有哪些问题尚未考虑周全：</p>
<p>1. 数据格式不统一的问题。客户端逻辑需要读取数据（美术制作的模型，凸包，场景地形等等），服务器逻辑也需要读取数据（策划设置的触发器，技能，NPC，怪等等）。这些数据虽然都是由编辑器提供的，但是基于诸多理由，编辑阶段的数据与实际运行阶段的数据完全可能并不相同。如何在运行时动态的将数据转化为客户端及服务器认可的数据是一个问题。特别的，如果采用光照图之类的技术进行渲染，编辑器在导出数据的时候，还要进行费时的光照图计算；类似的还有通过图的计算，也是颇为耗费时间的工作。如何让编辑器中热启动的服务器和客户端能够在足够短的时间里获得准备好的数据，是一个需要仔细考虑的问题。</p>
<p>谈到游戏数据的管理（创造，编辑，保存，读取），完全可以另列一文单独讨论，此前曾经拜读了MiloYip老大的《<a href="http://www.cnblogs.com/miloyip/archive/2010/04/12/1710179.html">从头开始思考游戏数据管理系统</a>》一文，其中提及了颇多关于游戏数据管理的思考，获益良多。在此对有兴趣的同学做以推荐。</p>
<p>2. 客户端逻辑并不简单只有输入逻辑，事实上还有玩家逻辑，动画播放，资源动态加载与释放，界面逻辑等等，都是复杂的模块。这些逻辑如何进行复用，接口需要如何设计，尚缺乏详尽的思考。</p>
<p>3. 由于本人对服务端逻辑的结构尚缺乏足够的了解，服务器逻辑的复用或存在许多未考虑的问题。尽管之前提过可以通过Proxy模式为服务器逻辑提供一个虚拟的运行环境，但是相信实际的困难依旧有许多。</p>
<p><strong>简要设计图</strong><strong></strong></p>
<p>我将上述的想法做了简单的汇总，下图是一个简要的设计概览：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/05/b08ef0f1c195.png"><img style="display: inline; border-width: 0px;" title="编辑器，服务端，客户端概念设计" src="http://www.windameister.org/blog/wp-content/uploads/2010/05/thumb1.png" border="0" alt="编辑器，服务端，客户端概念设计" width="546" height="789" /></a></p>
<p>上图假定服务器端运行在Linux下，客户端以及编辑器运行于Windows平台。</p>
<p>整体架构依赖于三个抽象接口，分别为服务端编辑器所依赖的<strong>数据库接口</strong>，客户端编辑器共同依赖的<strong>网络接口</strong>和<strong>用户输入接口</strong>。围绕这三个抽象接口，我们将许多耦合进行分解，使得服务器逻辑，客户端逻辑被独立成单独的模块，不仅可以用于服务端程序或者客户端程序当中，还可以复用于编辑器中，从而使得编辑器拥有了伪造一个服务端和客户端的能力，通过这种手段达到所编辑即所玩的目的。</p>
<p>（更进一步，通过将编辑器中的FakeNetwork层替换为RealNetwork网络层，可以让编辑器直接连接到实际运行的服务器上，我们便得以在编辑器中直接启动一个客户端程序）</p>
<p>基于上述架构进行开发时，开发人员在许多地方避免了重复代码的编写（比如服务器端逻辑决定了NPC的行为，则编辑器编写者无需在编辑器中编写任何伪造NPC行为的代码，我们可以直接获得真实的服务器端NPC的表现——通过源于实际的服务端逻辑编写者编写的服务端逻辑代码；类似的，客户端逻辑决定了界面，消息响应等，既可用于实际的客户端，也可用于编辑器中模拟客户端的行为）</p>
<p><strong>后记</strong><strong></strong></p>
<p>本文实际上是针对网络游戏编辑器设计的一个尚未成熟的思路。倘若按此思路进行实做，还有不少需要考虑、充实的细节，以及需要克服的困难。但从缩短游戏开发流程，提高代码复用率的角度，本人以为文中所述的方向应当是日后网络游戏开发的趋势。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.windameister.org/blog/2010/05/16/a-simple-approach-to-logic-and-abstraction-for-online-game-editor/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Hermite Curve 在绘制轨迹中的应用</title>
		<link>http://www.windameister.org/blog/2010/05/12/hermite-curve-usage-in-trail-drawing/</link>
		<comments>http://www.windameister.org/blog/2010/05/12/hermite-curve-usage-in-trail-drawing/#comments</comments>
		<pubDate>Wed, 12 May 2010 13:28:14 +0000</pubDate>
		<dc:creator>windam</dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[3d]]></category>
		<category><![CDATA[Hermite Spline]]></category>
		<category><![CDATA[Ogre]]></category>
		<category><![CDATA[样条曲线]]></category>
		<category><![CDATA[轨迹]]></category>

		<guid isPermaLink="false">http://www.windameister.org/blog/2010/05/12/hermite-curve-%e5%9c%a8%e7%bb%98%e5%88%b6%e8%bd%a8%e8%bf%b9%e4%b8%ad%e7%9a%84%e5%ba%94%e7%94%a8/</guid>
		<description><![CDATA[&#160; 在3D游戏的粒子系统中，往往可能有这样一类需求——伴随着人的动作，一条光带划过，或者伴随着武器的劈砍动作，一条刀光划过。 如果我们简单的在每次Tick的时候采样当前的位置朝向，并创建一截新的面片，往往会形成类似于如下图的粗陋效果： 之所以会产生一截一截的感觉，是由于在每次进行采样的时候，我们只能得到当前时刻的位置信息。因此尽管在从上一次到本次采样的过程中，轨迹实际经过的路程是一条平滑的曲线，可由于每一帧之间的时间不可能小到足够提供平滑曲线的采样精度，我们所能获得的总是一截截的折线。 怎样才能够通过为数不多的关键点，创造出一条平滑的曲线来呢？ Charles Hermite 提出的Hermite Curve解决了上面的问题。通过一系列的三维空间关键点，可以得到每个点处的切线，再经过Hermite Base Function的多项式计算，则可以得到插值点。 如图： 设P1，P2为平面上的两个点，T1，T2为两点处的切线，由点及点的切线，Hermite Spline就可以给出一个平滑过渡的曲线。 Hermite Base 多项式的参数如下表，其中t是从上一个点到当前点的插值系数。上一个点处为0，当前点为1。 Ogre在其Ogre::SimpleSpline类中实现了HermiteCurve。感兴趣的同学可以做以参考。 &#160; &#160; expanded factorized h00(t) t3 − 3t2 + 1 1 + 2t)(1 − t)2 h10(t) t3 − 2t2 + t t(1 − t)2 h01(t) − 2t3 + 3t2 t2(3 − 2t) h11(t) t3 − t2 t2(t [...]]]></description>
			<content:encoded><![CDATA[<p>&#160;</p>
<p>在3D游戏的粒子系统中，往往可能有这样一类需求——伴随着人的动作，一条光带划过，或者伴随着武器的劈砍动作，一条刀光划过。</p>
<p>如果我们简单的在每次Tick的时候采样当前的位置朝向，并创建一截新的面片，往往会形成类似于如下图的粗陋效果：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/05/2ae8af3cdeb4.png"><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; margin-left: 0px; border-left-width: 0px; margin-right: 0px" title="不连续轨迹" border="0" alt="不连续轨迹" align="left" src="http://www.windameister.org/blog/wp-content/uploads/2010/05/thumb.png" width="244" height="150" /></a> </p>
<p>之所以会产生一截一截的感觉，是由于在每次进行采样的时候，我们只能得到当前时刻的位置信息。因此尽管在从上一次到本次采样的过程中，轨迹实际经过的路程是一条平滑的曲线，可由于每一帧之间的时间不可能小到足够提供平滑曲线的采样精度，我们所能获得的总是一截截的折线。</p>
<p>怎样才能够通过为数不多的关键点，创造出一条平滑的曲线来呢？</p>
<p>Charles Hermite 提出的<a href="http://en.wikipedia.org/wiki/Cubic_Hermite_spline">Hermite Curve</a>解决了上面的问题。通过一系列的三维空间关键点，可以得到每个点处的切线，再经过Hermite Base Function的多项式计算，则可以得到插值点。</p>
<p>如图：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/05/HermiteCurve.png"><img style="border-right-width: 0px; display: inline; border-top-width: 0px; border-bottom-width: 0px; margin-left: 0px; border-left-width: 0px; margin-right: 0px" title="HermiteCurve示意" border="0" alt="HermiteCurve示意" align="left" src="http://www.windameister.org/blog/wp-content/uploads/2010/05/HermiteCurve_thumb.png" width="244" height="170" /></a> </p>
<p>设P1，P2为平面上的两个点，T1，T2为两点处的切线，由点及点的切线，Hermite Spline就可以给出一个平滑过渡的曲线。</p>
<p>Hermite Base 多项式的参数如下表，其中t是从上一个点到当前点的插值系数。上一个点处为0，当前点为1。 </p>
<p>Ogre在其<a href="http://www.ogre3d.org/docs/api/html/classOgre_1_1SimpleSpline.html">Ogre::SimpleSpline</a>类中实现了HermiteCurve。感兴趣的同学可以做以参考。</p>
<p>&#160; </p>
<table border="0" cellspacing="0" cellpadding="2" width="400">
<tbody>
<tr>
<td valign="top" width="133">&#160;</td>
<td valign="top" width="133">expanded</td>
<td valign="top" width="133">factorized</td>
</tr>
<tr>
<td valign="top" width="133"><i>h</i><sub>00</sub>(<i>t</i>)</td>
<td valign="top" width="133"><i>t</i><sup>3</sup> − 3<i>t</i><sup>2</sup> + 1</td>
<td valign="top" width="133">1 + 2<i>t</i>)(1 − <i>t</i>)<sup>2</sup></td>
</tr>
<tr>
<td valign="top" width="133"><i>h</i><sub>10</sub>(<i>t</i>)</td>
<td valign="top" width="133"><i>t</i><sup>3</sup> − 2<i>t</i><sup>2</sup> + <i>t</i></td>
<td valign="top" width="133"><i>t</i>(1 − <i>t</i>)<sup>2</sup></td>
</tr>
<tr>
<td valign="top" width="133"><i>h</i><sub>01</sub>(<i>t</i>)</td>
<td valign="top" width="133">− 2<i>t</i><sup>3</sup> + 3<i>t</i><sup>2</sup></td>
<td valign="top" width="133"><i>t</i><sup>2</sup>(3 − 2<i>t</i>)</td>
</tr>
<tr>
<td valign="top" width="133"><i>h</i><sub>11</sub>(<i>t</i>)</td>
<td valign="top" width="133"><i>t</i><sup>3</sup> − <i>t</i><sup>2</sup></td>
<td valign="top" width="133"><i>t</i><sup>2</sup>(<i>t</i> − 1)</td>
</tr>
</tbody>
</table>
<p>事实上，上述做法也不过就是对Hermite Curve类型的样条曲线的利用而已。样条曲线实际上并不陌生，贝塞尔曲线就是一种我们关注最多的样条曲线。尽管其不适用于我们前述的场合——原因是贝塞尔曲线通过控制点计算出的曲线并非一条依次经过控制点的曲线。如下图所示（图片来源于:<a href="http://escience.anu.edu.au/lecture/cg/Spline/printCG.en.html">http://escience.anu.edu.au/lecture/cg/Spline/printCG.en.html</a>）：</p>
<p><a href="http://www.windameister.org/blog/wp-content/uploads/2010/05/bezier_spline.png"><img style="border-bottom: 0px; border-left: 0px; display: inline; border-top: 0px; border-right: 0px" title="bezier_spline" border="0" alt="bezier_spline" src="http://www.windameister.org/blog/wp-content/uploads/2010/05/bezier_spline_thumb.png" width="244" height="177" /></a>&#160;</p>
<p>参考资料：</p>
<p>1. <a href="http://www.cubic.org/docs/hermite.htm">Hermite Curve Interpolation.</a> Hamburg (Germany), the 30th March 1998</p>
<p>2. <a href="http://en.wikipedia.org/wiki/Cubic_Hermite_spline">Cubic Hermite spline</a></p>
]]></content:encoded>
			<wfw:commentRss>http://www.windameister.org/blog/2010/05/12/hermite-curve-usage-in-trail-drawing/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

