引擎设计跟踪(九.14.2c) 最近一些小的更新
1. bump map与normal map
昨天拿了crytek sponza(http://www.crytek.com/cryengine/cryengine3/downloads)场景测试,
一开始用的是这里的模型(http://graphics.cs.williams.edu/data/meshes.xml)发现里面用到了1通道的bumpmap, 但是blade的shader并不支持, 如果要支持的话又要加材质和shader了, 所以用了这种hack, 只为了跑一下这个场景.
bumpmap是单通道的灰度图, 实际上是一张高度图, 在shader里面可以根据高度差得到法线扰动. 而法线贴图是切空间的相量.
bump map是对法线的偏移和扰动, 而normal map则是对法线的完全的替换.
法线贴图可以有两种途径产生, 一是高模烘焙, 二是通过高度图生成.第二种方法nvidia的photoshop插件里面就有(https://developer.nvidia.com/nvidia-texture-tools-adobe-photoshop).
原理不多说了,这里有个链接:
凹凸贴图(Bump Map)实现原理以及与法线贴图(Normal Map)的区别
bump map到normal map转换的方法从上面链接中的shader里面可以看出来, 即用高度图中的的导数dx, dy来构造法线. 下面是一个参考(原帖是有错误的求助帖, 回复中有SOBEL filter的代码片段):
https://www.opengl.org/discussion_boards/showthread.php/168864-HeightMap-to-NormalMap
更多的参考在这里(GIMP的法线贴图插件, 各种各样的, nxn的filter啊):
https://code.google.com/p/gimp-normalmap/source/browse/trunk/normalmap.c
目前blade使用了3x3的SOBEL filter. 这个转换只有实时转换用到了, 但理论上后台工具也可以直接用.
下面是用blade的导出插件导出的cretek sponza场景, 并在model viewer里面查看的效果, 后面做场景渲染效果的时候可以暂时先拿它来测试:
这个场景比较复杂, 用它测试max导出插件, 也发现了在导出切空间时的某些bug, 比如计算切空间时, 三角形两个边的向量, 是带长度的向量, 不能单位化, 否则对于狭长的三角形, 计算错误会暴露出来.
另外,与话题无关的, 还有两个诡异的问题, 上面的两个模型(.obj)在max里都是这样: 一个材质sponza_bricks, 它的diffuse map和normal map是同一张diffuse贴图: spona_bricks_a_diff.tga, 但是明明有对应的法线贴图文件在啊, 而且用这个导出模型的话, 渲染结果明显不对, 害得又被坑了, 改成正确的贴图就好了.
而且贴图明明含有specular map, 但是材质里面又没有设置specular map, 这个我也暂时没管了.
2.通过上面的模型, 也测试出了几个material LOD的几个小bug:
a.虽然模型系统的材质是以单个子mesh为单位更新的,但是子mesh交给materialLOD updater的世界坐标, 是父model的坐标, 即所有的子mesh都使用的相同的坐标. 这样如果一个model太大, 位置的精度会有问题, 现已改为子mesh真正的世界坐标, 这样材质LOD的精度真正达到单个子mesh级别.
b.判断材质LOD的距离, 之前用的是相机和物体中心之间的距离.当物体非常大的时候, 相机已经在物体的内部了, 但是距离物体中心还很远, 导致LOD切换出了问题, 这个需要用距离减去物体半径(虽然接口已经定义了半径/AABB但是一直没有用).
c.有的submesh有多个部件, 而且相距很远(比如场景的两端),但是他们属于同一个submesh(共享同一个材质), 导致这个submesh非常大, 相机一直在其内部. 这样的材质LOD更新粒度太大, 导致材质LOD没有机会切换. 这个要改导出插件. 因为现在的导出方式是按材质划分submesh, 合并材质相同的object, 这个需要改为按material + object划分出submesh, 相同material的object分开导出而不合并. 只要material相同的object在导出时, 分开的多个submesh连续在一起, 同时在渲染时按顺序绘制. 这样与之前相比, 从一个材质对应一个drawcall, 变成一个材质对应多个连续drawcall, 结果将是多几个drawcall, 也没有多余的材质切换, 是可接受的折衷方案.
3.UV坐标系改用OGL坐标系.
因为blade使用的坐标系是右手系, 但是uv使用的是dx的坐标系(u向右v向下), 这导致"默认的"切空间是左手系, 有点不爽. 所以就改成OGL的坐标系, 即u向右v向上.
这样默认的切空间变成右手系, 不过在实际中切空间是使用左手系还是右手系, 同时要看normal map是哪一种了.windows下大多游戏的法线贴图都是左手系.
左手系和右手系的normalmap是不一样的,他们的G通道是相反的.记得在网上看过两张图片的对比,具体链接忘了.
所以也在导出插件里面添加了手相选项. 一般来说,对于一个项目, 这些参数(手相之类)都是预定好的规范, 然后根据规范制作对应的模型和贴图, 这个时候这个手相选项就是固定的, 没有太大用处.
但是对于想使用3方现有模型和贴图的时候(比如我目前在网络上下载的各种模型), 由于各种情况都有, 这个时候可能需要选择导出手相, 使切空间匹配法线贴图.
而对于上面的bump map 转的normal map, 因为是运行时生成, 所以导出时的切空间可以选择任意坐标系, 比如目前导出时选择右手系, bump转normal的时候也使用右手系就可以了.
另外,blade对于uv的处理, 之前的想法是这样的: 使用固定的uv坐标系, 如果使用的uv, 或者图像的内存布局(有的是从低向上, 有的是从上到下)跟渲染API期望的不一样, 就翻转纹理图像的内容, 这样对于不同的API, uv和shader都不用改了.
虽然这个想法理论上没有问题, 但是实际上非常二: 运行时翻转图像太耗时.. 现在已经改成加载时翻转模型的纹理坐标(v坐标)了, 这样最终效果也一样.
因为OGL的uv坐标系要求图像是倒着存放的(原点在左下角), 如果加载的纹理不是这种布局(原点在左上角), 那么就翻转纹理坐标的v值. 比如DDS图像格式,其原点在左上角而不是左下角.
在用MFC+GDI做编辑器的过程中, 学习到GDI的bitmap header, 或者通用的bitmap header可以根据图像高度biHeight的符号来动态区分其存放格式http://msdn.microsoft.com/en-us/library/aa930622.aspx),
而在使用FreeImage的过程中发现, FreeImage加载的位图都是垂直倒放的, 这对于OGL有着先天优势.
4.threading utility
因为C++11里面已经有简单的thread了, 但是blade是基于C++98/03写的, 没有使用任何新特性. 因为Blade内部已经使用了Intel TBB, 多线程这个功能因为一直没有直接用到(除了自己写的readwritelock用到了mutex lock), 所以没加.但是考虑到以后用户的可能需求, 所以先放上.
本来的思路是写成C++11兼容的接口, 这样用户可以在不支持C++11编译器上用blade自带的thread, 接口也能保持一致, 不用改代码.
但是发现thread的某些功能使用了C++11的语法新特性, 所以这种想法不可行, 于是就用blade当前的风格简单封装了condition variable, mutex, thread, 放在了foundation库里面.目前做了windows系列和*nix (pthread)两种实现.pthread对应的功能都有, 只是简单的封装, windows下新版本的crt也有, 但是为了兼容性, 是自己写的.
目前只做了简单的测试, 估计还有N多bug, 暂时先这样吧, 嗯.
5. IK预研
目前找到的参考有:
http://freespace.virgin.net/hugo.elias/models/m_ik.htm
http://freespace.virgin.net/hugo.elias/models/m_ik2.htm
http://billbaxter.com/courses/290/html/img0.htm
http://graphics.ucsd.edu/courses/cse169_w04/welman.pdf
http://graphics.ucsd.edu/courses/cse169_w05/CSE169_12.ppt
http://graphics.ucsd.edu/courses/cse169_w05/CSE169_13.ppt
循环坐标下降(CCD)算法中对骨骼动画中膝盖等关节的特殊处理 很高兴国内已经有人在做^^.
另外还有 DOOM3 source code, 里面有IK的代码.
据了解 IK常用的方案有: Jacobian Transpose, CCD( cyclic coordinate descent), 和Analytical Method.
前两者是迭代逼近的解法, 而其中CCD更简单易懂, 而且有效.
Jacobian Transpose: http://www.math.ucsd.edu/~sbuss/ResearchWeb/ikmethods/iksurvey.pdf
CCD: https://sites.google.com/site/auraliusproject/ccd-algorithm
Analytical Method: http://www.ryanjuckett.com/programming/analytic-two-bone-ik-in-2d/
最后一个是几何分析的方法, 适用于简单的情况. 目前从doom3的代码看, 它好像用的这种方式(貌似没看到迭代).
blade的计划支持腿部IK, 还有臂部IK, 必要时加上头发辫子等等, 这个类似doom3的IK_Walk和IK_Reach, 但是臂部IK应该要比doom3更复杂.
后面还是继续IK的研究, 工作很忙, 先抽空看看资料吧, 进度会很慢.