经过一段时间的编码,终于完成了一个效果还算可以的Demo

发一些图片出来,如果对此感兴趣我们可以一起多多交流。(所有图片均缩小显示,可右键另存为看原图)
其中主要用了基于物理的渲染,物理摄像机,以及屏幕空间sss(其中皮肤半透部分因需求没有做,不是完整的sss,以后有机会完善之)

来张整体效果图

这个是控制参数面板

左边纹理从上到下分别是 1.世界空间中的法线 2.线性深度(viewPos.z/farClip)3.Ambient 4. Diffuse 5.Specular 6.最终合成(不含DOF,SSS等)
右边是两盏SpotLight对应的ShadowMap  

 

 

下面是一些参数调整后的效果对比图

 

 

posted @ 2012-05-15 10:55 醉翁之家 阅读(214) 评论(5) 编辑
注:该方法是以CryEngine所使用的方法为基础略微改进而来的。

 先看一下效果图。

最终效果

显示顶点的TBN(T、B、N分别用R、G、B颜色轴显示)

所使用的法线贴图

具体办法如下:

1.导出Mesh所有三角面顶点的位置 Point3 pos、纹理坐标 Point2 uv以及对应的索引值。
      导出方法略。注意:位置个数numPos和纹理坐标个数numUV通常不相等。索引个数为面数numFaces*3。
2.遍历Mesh的每一个三角面,用三角面的三个pos和三个uv 计算每个三角面的TBN。
    计算方法为:设三角形的三个顶点为Point3 pos[3], 其分别对应的纹理坐标为Point2 uv[3]。那么两条边为
        Point3 edgeA = pos[1] - pos[0];
        Point3 edgeB = pos[2] - pos[0];
    计算法线
        Point3 nor = FNormalize( edgeA ^ edgeB );
    计算
        float deltaU1 = uvw[1].x - uvw[0].x;
        float deltaU2 = uvw[2].x - uvw[0].x;
        float deltaV1 = uvw[1].y - uvw[0].y;
        float deltaV2 = uvw[2].y - uvw[0].y;
        float div = ( deltaU1 * deltaV2 - deltaU2 * deltaV1 );
    根据div的值算tan和bin, 使用三角形面积作为权值。
        Point3 tan = Point3( 1.0f, 0.0f, 0.0f );
        Point3 bin = Point3( 0.0f, 1.0f, 0.0f );
        if( div != 0.0 )
        {
            float areaW = fabsf( div ); // 三角形面积*2
            float a =  deltaV2 / div;
            float b = -deltaV1 / div;
            float c = -deltaU2 / div;
            float d =  deltaU1 / div;
            tag = FNormalize( edgeA*a + edgeB*b ) * areaW;
            bin = FNormalize( edgeA*c + edgeB*d ) * areaW;
        }
3.遍历Mesh每一个三角面,用三角面的N、三个顶点Pos 以及 光滑组信息,计算每个顶点的法线.
    方法为:将共享该顶点的所有三角面法线根据平滑组信息分组相加(可能会有多组),同时为了解决L型问题,需要用三角形中顶点的两条边的夹角做权重乘以面法线。
    如果只导出法线,到此就可以结束了。
    注意此时的顶点法线数numVerNor很可能已经 > 位置个数numPos了,换句话说已经根据光滑组分裂了顶点。
4.遍历Mesh每一个三角面,计算三角形中每一个顶点的T和B。计算的方法和法线一样。(要根据顶点T、B,以及uv来决定是否需要分裂顶点)
    顶点累加结果verTBN.tan和verTBN.bin初始为Point3(0,0,0),verTBN.nor为第3步算出的顶点法线。计算时仍然需要使用夹角作为权重。
    注意这步完成后得到的T、B、N的数量是相等的,且>=第3步中的numVerNor。
    4.1 如果存在UV镜像,那么分裂顶点
        判定是否存在UV镜像的方法是;
            面TBN的手向性 bool faceParity = DotProd( faceTBN.tan^faceTBN.bin, faceTBN.nor ) > 0.0f;
            顶点TBN的手向性 bool verParity =  DotProd( verTBN.tan^verTBN.bin, verTBN.nor ) > 0.0f; 。
            如果 faceParity != verParity 那么存在uv镜像,需分裂顶点。
    4.2 如果UV旋转角度大于90,那么分裂顶点
        判定方法为:
            Point3 vrefUV = verTBN.tan + verTBN.bin;
            Point3 vRotHalf = vrefUV - faceTBN.nor * DotProd(faceTBN.nor, vrefUV);
            如果 DotProd( (faceTBN.tag + faceTBN.bin), vRotHalf ) <= 0.0f 那么 uv旋转角度大于90度,需分裂顶点
5.用思密特正交化处理顶点TBN,并记录TBN的手向性到T.w中。
    将U和V分别投影到N所在的平面,然后单位化得到最终的verTBN.tan和verTBN.bin.
    通过叉乘verTBN.tan和verTBN.bin计算新的newNor,并单位化。
    检查新法线和第4步计算法线之间的夹角,即最终顶点TBN的手向性。
    if( DotProd( newNor, verTBN.nor ) < 0.0f )
    {
        newNor *= -1.0f;
        verTBN.tan.w = -1.0f;

    } 

 

下面两张图是比较有用的 一张是uv图,一张是法线图。

                                 

uv图可用于直观的检查导出的顶点TBN是否正确。检查方法为,看顶点的T轴和B轴是否分别和UV图中U和V的箭头方向一致。如果一致则正确。

法线图可以检查UV镜像和UV旋转的问题。检查方法为,打上灯光看光照效果是否正确,即有无明显接缝。

 

 下面是一些模型的测试截图

有UV镜像的一个面片 最终效果

模型顶点、面以及uv分布情况

顶点TBN(注意T轴和B轴分别与U和V的方向一至)

 

所使用的法线贴图

3DMax中的茶壶 最终效果

茶壶顶点TBN

茶壶盖顶部特写,有uv旋转

posted @ 2011-12-08 10:11 醉翁之家 阅读(1323) 评论(2) 编辑

转自 http://clkrst.itpub.net/post/137/41162

linux下跑得一直很好的程序,到了windows下面就跑不起来了。内存异常,检查了一下,很快发现是因为在主程序中释放了一块在DLL中分配的内存,这种问题虽然早就知道了,但是一直没有仔细考虑过,所以今天就深入研究了一下。



在linux下,每个进程只有一个heap,在任何一个动态库模块so中通过new或者malloc来分配内存的时候都是从这个唯一的heap中分配的,那么自然你在其它随便什么地方都可以释放。这个模型是简单的。

但是在windows下面,问题变得复杂了。

1、windows允许一个进程中有多个heap,那么这样就需要指明一块内存要在哪个heap上分配,win32的HeapAlloc函数就是这样设计的,给出一个heap的句柄,给出一个size,然后返回一个指针。每个进程都至少有一个主heap,可以通过GetProcessHeap来获得,其它的堆,可以通过GetProcessHeaps取到。同样,内存释放的时候通过HeapFree来完成,还是需要指定一个堆。

2、这样的设计显然是比较灵活的,但是问题在于这样的话,每次分配内存的时候就必须要显式的指定一个heap,对于crt中的new/malloc,显然需要特殊处理。那么如何处理就取决于crt的实现了。vc的crt是创建了一个单独的heap,叫做__crtheap,它对于用户是看不见的,但是在new/malloc的实现中,都是用HeapAlloc在这个__crtheap上分配的,也就是说malloc(size)基本上可以认为等同于HeapAlloc(__crtheap, size)(当然实际上crt内部还要维护一些内存管理的数据结构,所以并不是每次malloc都必然会触发HeapAlloc),这样new/malloc就和windows的heap机制吻合了。(这里说的是vc的crt实现,我不知道其它crt实现是否如此)

3、如果一个进程需要动态库支持,系统在加载dll的时候,在dll的启动代码_DllMainCRTStartup中,会创建这个__crtheap,所以理论上有多少个dll,就有多少个__crtheap。最后主进程的mainCRTStartup 中还会创建一个为主进程服务的__crtheap。(由于顺序总是先加载dll,然后才启动main进程,所以你可以看到各个dll的__crtheap地址比较小,而主进程的__crtheap比较大,当然排在最前面的堆是每个进程的主heap。)

4、从上面的分析中可以看出,对于crt来说,由于每个dll都有自己的heap,所以每个dll通过new/malloc分配的内存都是在自己dll内部的那个heap上用HeapAlloc来分配的,而如果你想在其它模块中释放,那么在释放的时候HeapFree就会失败了,因为各个模块的__crtheap是不一样的。

这样,基本上事情就比较清楚了,在windows下一个进程存在着多个heap,除了一个主heap外,还有很多的__crtheap,用来处理通过c/c++的运行库进行的内存操作。所以使用new/malloc来分配的内存实际上都是局部的,可以在多个dll中共享,但是却必须是谁申请谁释放。这个是windows下的一个规则。以前知道这个规则,但是不知道为什么,现在算是比较明白了。(当然如果在dll内部使用HeapAlloc(GetProcessHeap(), size)来分配的内存是可以在dll以外释放的,因为这时内存分配在全局的主heap上,而不是分配在dll自己的__crtheap上)

posted @ 2011-08-18 15:57 醉翁之家 阅读(107) 评论(0) 编辑

http://blog.sina.com.cn/s/blog_4c20b377010007yr.html

本文的目的是: 简单说明如何把zlib加入到MFC程序中,提供内存压缩功能.

1. 如何获得zlib

zlib的主页是:http://www.zlib.net/

2. 用VC++6.0打开

把下载的源代码解压打开,VC6.0的工程已经建好了,在\projects\visualc6. 双击zlib.dsw, 可以在VC++6.0中看到里面有3个工程: zlib 是库文件(编译设置选中 win32 lib debug / release), 工程example 是如何使用 zlib.lib 的示例, 工程minigzip 是如何用 zlib 提供的函数读写.gz文件的示例(*.gz的文件一般Linux下比较常用).

3. 如何加入到我的工程

编译好 zlib.lib 后, 你就得到了调用一个静态库所需要的所有文件了(zlib.lib, zlib.h, zconf.h). 如何调用静态库不用我说了吧.

4. 用zlib能干什么

先来看看 zlib 都提供了那些函数, 都在zlib.h中,看到一堆宏不要晕,其实都是为了兼容各种编译器和一些类型定义.死死抓住那些主要的函数的原型声明就不会受到这些东西的影响了.

关键的函数有那么几个:

(1)int compress (Bytef *dest,   uLongf *destLen, const Bytef *source, uLong sourceLen);

把源缓冲压缩成目的缓冲, 就那么简单, 一个函数搞定

(2) int compress2 (Bytef *dest,   uLongf *destLen,const Bytef *source, uLong sourceLen,int level);

功能和上一个函数一样,都一个参数可以指定压缩质量和压缩数度之间的关系(0-9)不敢肯定这个参数的话不用太在意它,明白一个道理就好了: 要想得到高的压缩比就要多花时间

(3) uLong compressBound (uLong sourceLen);

计算需要的缓冲区长度. 假设你在压缩之前就想知道你的产度为 sourcelen 的数据压缩后有多大, 可调用这个函数计算一下,这个函数并不能得到精确的结果,但是它可以保证实际输出长度肯定小于它计算出来的长度

(4) int uncompress (Bytef *dest,   uLongf *destLen,const Bytef *source, uLong sourceLen);

解压缩(看名字就知道了:)

(5) deflateInit() + deflate() + deflateEnd()

3个函数结合使用完成压缩功能,具体用法看 example.c 的 test_deflate()函数. 其实 compress() 函数内部就是用这3个函数实现的(工程 zlib 的 compress.c 文件)

(6) inflateInit() + inflate() + inflateEnd()

和(5)类似,完成解压缩功能.

(7) gz开头的函数. 用来操作*.gz的文件,和文件stdio调用方式类似. 想知道怎么用的话看example.c 的 test_gzio() 函数,很easy.

(8) 其他诸如获得版本等函数就不说了.

总结: 其实只要有了compress() 和uncompress() 两个函数,在大多数应用中就足够了.

题外话: 我最初看到zlib的源代码时被好多宏吓倒了,呵呵,后来仔细看下去才发现原来接口那么简单. 至于那些英文说明也没想象中的那么难懂.只要有尝试的勇气,总能有些收获.

 

posted @ 2010-11-17 14:49 醉翁之家 阅读(224) 评论(0) 编辑

项目大事记
    2007.3 项目基本上算是进入真正的开发阶段
    2007.6 新的投资人老陈,老谭加入
    2007.12.31 第一次面向玩家测试,程序频繁崩溃
    2008.1-2009.3 研发由短平快迅速上市的方针改为做高品质的产品
    2009.3-2009.6 和山东某公司合作运营,效果不理想
    2009.3 拟被某上市公司收购,未成功
    2009.4.18 第一次面向玩家的大规模测试,效果比较理想但人数上不去,内存占用太高
    2009.7 被另一公司收购
    2009.12 换游戏名称第一次面向玩家的大规模测试,客户端报错平凡,服务器宕机,人数上不去
    2010.3 再次正式上线运营,效果不理想

我的情况
    我于2008年5月加入公司,进入公司主要负责世界编辑器、粒子编辑器、物理系统的编写。我并没有直接参与到游戏客户端代码的编写。大多数工作都是在负责游戏相关工具的开发以及技术难点的公关工作。

我的责任:
    一、没有过多的参与到游戏本身代码的编写和监管之中。
    二、没有顶住来自市场或投资商的不切实际的需求变更。

程序问题
    一、程序底层没有细致规划,各模块耦合太大。
    由于游戏最初的定位非常简单,只是简单将美术资源替换和修改,以及非常简单的程序功能的改动(源有代码为2005年左右的基于DX8的代码),然后马上上线运营。不过经过市场检验发现游戏程序根本无法达到基本要求。于是开始大刀阔斧的改程序加功能。当然这些改动还是有一定的成效的,不过对于一款商业游戏软件来说还是有很大的距离。当我加入项目组时,整个游戏连世界编辑都没有,美术在设计场景时还要两个人一起,一个用人在游戏里面跑,一个用笔记录对应的坐标点。。。。。。无语。最初游戏中连最基本的场景管理等都没有。而且各个模块之间基本上没有界限,该分开的地方不分开(一个与脚本相关的CPP文件居然有24000多行。。。。。。),该抽象的地方不抽象。可以这样说大部分人都还停留在以面向过程的程序设计思想在写程序(虽然项目后期有了很大改观,可是参与者基本上都已经无能为力了)。由于各模块的偶合太大,也就造成了以后对任何功能的修改和添加都可能引起一连串的BUG。
    二、第三方库的选择和使用都有很大问题。
    每个第三方库都有自己的特点和使用技巧,但是在我们的项目中对第三方库的使用很有问题。比如CEGUI,到项目结束我们团队里面的人还没有一个人敢拍胸脯说“我能把CEGUI用好!”。还有lua脚本的使用也是如此。
    三、需求不断变化已有功能不断返工。
    这个不太好说,如果有一个项目统管者我想应该好很多吧。
    四、程序员水平参差不齐,编程经验及其缺乏。
    这个没有什么好说的。公司的待遇只有这么多,招聘的人员水平也只有这个样子(大部分是没有从业经验或才从学校毕业的)。唉只有叹息。。。。。。不过团队中的人能在这样的框架下写成这样效果我已经感到欣慰了。至少他们通过这一次锻炼提高了非常多。
    五、具体功能的实现缺乏细致的设计规划以及充足的时间来具体实施。
    具体功能的实现缺乏规划这个是一个比较普遍的情况。当然这有如下几个主要原因a、需求提的太匆忙,给程序留出的时间太少。这一点我的体会非常深,从我进入该项目组以来整个项目组的人都是在不断的赶新功能,缺乏一些大的规划和设计。这些功能基本上都是想到一个感觉不错就立马要加,而且都是说,没有这个就不能上线。等这个功能完成了又会要求做另外一个类似情况的功能,如此循环程序就不断重构原有代码和添加新代码。b、具体实现人员由于缺乏经验或太过于繁忙没有去设计和规划,只是一味的为完成任务而完成任务。这也是造成一旦需求有所变化或调整,那么代码改动会非常大。c、游戏功能的添加与取消太过随意。这个问题我的影响特别深,游戏原来有基于二维格子的自动寻路功能,没有精细碰撞,没有跳跃等,一段时间后提出要求说需要精细碰撞和跳跃(由于时间等原因去掉自动寻路),好既然决定那么程序就开始动吧,程序写碰撞算法,美术准备碰撞模型等。过来一段时间这些基本ok后,另一个运营商要求自动寻路必须加,好又再次把自动寻路(本来想直接加3D自动寻路的结果又一次被否决了,还是使用2D寻路,实际效果并不好勉强可用。。。。。)加上去。
    六、项目前期及中期基本上没有必要的测试。
    我们的项目可以说能走到今天简直是奇迹。在项目的前期和中期基本上没有专门的测试。我所了解到的测试基本都是程序员自己实现了功能后自己简单的测试(有的可能根本就没有测试过),最多在加上相关的策划人员配合测试。经过几次阶段性开发后,公司内部才开始组织专门的测试部门进行相对系统的测试。在短短的有限的一两个月中测试出的BUG个数就高达2800多个,当然这里面还不包括那些我们已知但由于底层等原因无法彻底解决的好几个重大Bug(比如客户端的CEGUI的使用,服务器端的数据库访问机制和效率问题等)。

posted @ 2010-06-09 16:46 醉翁之家 阅读(97) 评论(1) 编辑
摘要: 一位资深经理人的职业生涯感悟序言在担任公司高管的几年间,我面试过数以百计的各个层面的员工,其中最让我感到遗憾的一个现象就是很多人有着非常好的素质,甚至有的还是名校的毕业生,因为不懂得去规划自己的职业,在工作多年后,依然拿着微薄的薪水,为了一份好一点的工作而奔波。很多这样的人,他们只要稍微修正一下自己的职业方向,就能够在职业发展上走得更从容。有一次一个大连理工大学的研究生,好像是学电子的,来应聘我们...阅读全文
posted @ 2010-06-07 10:27 醉翁之家 阅读(79) 评论(0) 编辑
摘要: 文/巨人投资总监许怡然  经历太多次创业,发现创业实在太难,一开始我认为是我的运气稍微差了一点,每一次创业失败的原因都不尽相同,使我经历了各种各样的创业痛苦,不过后来看看我周围跟我一起创业的弟兄们,发现创业的人生就是如此。在此不乏调侃地写下网游创业失败全攻略给各位共享,各位有过创业失败经历的人可能会有“心有戚戚焉”的感觉,至今还没有经历过失败的朋友们,你们也可能也会产生类似...阅读全文
posted @ 2010-06-07 10:21 醉翁之家 阅读(60) 评论(0) 编辑
摘要: 高云卓前言:投资人并不挑剔2009年游戏圈里比较大的一场资本并购是博瑞传播4.4亿收购梦工厂。名气不大的梦工厂之所以能卖如此高价,是因为其游戏的确能赚钱。梦工厂经历了5年的联运生涯后,突然发现联运市场逐渐走进只赚吆喝不赚钱的尴尬境地,于是决定代理一些产品,收购一些团队,从而保持一个传统网络游戏公司的高速发展。也正因为此,笔者有幸接触到一些研发公司和研发团队,评测完成不少产品,其间感慨颇多,汇集成稿...阅读全文
posted @ 2010-06-07 10:19 醉翁之家 阅读(66) 评论(0) 编辑
摘要: http://mosq.blog.sohu.com/113680415.html建议准备做项目和正在做项目的都来看看,吸取一下教训。处女项目就失败了,对团队每一个人打击都是很大的。1.过多的工作压到了同一个人身上在很多小公司[或小项目组]里,总有那么一个核心程序员,负担了几乎所有的编程任务。这个人即使是个天才,也被繁重的工作压的没有学习和自我提高的时间。比如设计模式这样的思考方式也是近几年才进入中...阅读全文
posted @ 2010-06-04 18:33 醉翁之家 阅读(147) 评论(1) 编辑
摘要: 本算法是以灰度图为例,彩色图像也很简单大家自己修改即可.当然对浮点纹理也有效果.该算法在地形的高度图处理中已被使用,效率和效果还算满意. 先看效果原始图片 最近点采样线性采样基本原理就是缩放前后的图片相对应的位置的UV坐标相同即: srcX/srcWidth = dstX/dstWidth srcY/srcHeight = dstY/dstHeight推出srcX = dstX * srcWidt...阅读全文
posted @ 2010-05-31 14:17 醉翁之家 阅读(2271) 评论(0) 编辑