最近把gizmo helper的绘制做好了.

1.为了复用代码,写了utility来创建sphere, cube, cylinder, plane, ring(line), circle(solid) 这些基本物体, 顺便把天空球的创建代码改用utility函数,以后DS的灯光球和椎体等等也可以复用了.

2.本来很简单的以为使用正交投影(orthographic projection)就可以画出大小不变的gizmo.但是却忽略了一个问题: 因为helper的世界变换矩阵跟选中的物体的一样,所以理论上gizmo的轴朝向是跟物体的轴(透视投影后的)朝向一致,位置也和物体(透视)投影后的坐标一致. 但是用正交投影以后,发现旋转和位置都不匹配, 仅仅是大小不变. 比如下图,同一个点, 投影模式不一样,那么最终的位置也不一样:

所以正确的做法应该是用透视投影,并保持投影后的大小不变.思路就是根据投影后的大小反推原始参数,比如这里的求助贴: http://gamedev.stackexchange.com/questions/24968/constant-size-geometries 作者自问自答,使用的是投影大小反推缩放值. 我用的方法类似,也比较简单容易理解:使helper和摄像机保持固定距离.

其实这个固定距离跟投影后的固定大小一样, 是一个magic值,只不过投影后的大小这个值更直观. 而且固定距离也可以先用投影后的固定大小算出来, 但是需要相机的投影参数. 我这里没有相机投影参数,所以做的很简单:

     //////////////////////////////////////////////////////////////////////////
void AxisGizmo::updateRelativeCamera(const Vector3& cameraPos,const Quaternion& cameraRotation)
{
//update gizmo transform //note: keep a constant distance form camera so that gizmo size looks unchanged
static const scalar DISTANCE = ;
const Vector3& pos = mTarget->getPosition();
Vector3 movedPos = cameraPos + (pos - cameraPos).getNormalizedVector()*DISTANCE; const Vector3& scale = Vector3::UNIT_ALL;
const Quaternion& rotate = mTarget->getRotation();
mState->setWorldTransform(movedPos, scale, rotate, cameraRotation);
}

3.上一步完成以后, Roation helper也有一个问题, 比如max或者maya的helper,最外层的那两个圆环一直朝向摄像机. 这个做法也有很多种,比如3D billboard,或者直接在投影后的位置画2D ring.
我用的是billboard的方法.首先, ring创建在view space (对象在object space的顶点坐标直接设置为在view space的坐标) 同时world transofrm为view.inverse(), 这样根据world*view=identity, 相当于顶点的原始坐标和直接在view space一致. 同时注意到view.inverse() (matrix 3x3) 正好就是camera的rotation matrix,所以billboard the world matrix计算如下:
world = gen_transform(pos, scale, matCameraRotation) 或者 world = gen_transform(pos, scale, quatCameraRotation)

         virtual void    setWorldTransform(const Vector3& pos,  const Vector3& scale, const Quaternion& rotation, const Quaternion& camearRotation)
{
AxisGizmoState::setWorldTransform(pos, scale, rotation, camearRotation);
//TODO: update view space ring transform ////note: the rings are created to orient the Vector3::UNIT_Z by default (vertices are in view space already)
////now just apply the rotation = inverse(viewMatrix33) = camera rotation, to make world*view == identity
Matrix44::generateTransform(mViewSpaceRingTransform, pos, scale, camearRotation);
}

其实做了这么多,就是为了在常规管线(world*view*projection)里去掉旋转,得到(identity + worldpos+viewTransPos)*projection即projected pos.于是在shader框架里又加了一组自定义的semantics(shader自动变量)为BillboardView/BillboardViewProjection/BillboardWorldView/BillboardWorldViewProjection, 直接跳过world*view的旋转,只使用位移, 这样可以方便在shader里面直接使用billbard.
感觉这样的billboard更高效,不需要反算world rotation,再乘上去:inverse(view)*view*projection, 而直接是identity.setpos(worldPos+viewTransformTranslate)*projection 去掉了2步无用的操作.而且用起来更方便了.因为这几个变量就像WORLD_MATRIX, WORLD_VIEW_PROJECTION一样,被封装到shader框架的自动变量里,shader框架会去自动计算和更新(如果用到的话),不用再像上面那样,起码要有一个billboard基类来复用CPU端代码,或者根据情况,逐个情况手动在CPU端计算.

唯一需要注意的是billboard物体的local space的几何顶点必须相当于直接在view space创建, 面向相机,一般为Z+或者Z-,比如我的右手系,view空间的视向量为Z-,那么billboard物体的面片在local space的朝向应该为Z+.因为目前没有billbard相关的系统所以没有测试用例,而这个里的gizmo helper用统一的shader也懒得再分出来一个shader专门测试shader变量,所以目前还是手动CPU代码计算变换.

4.注意max/maya中的rotation helper, 3个轴对应的圆圈, 只显示了前面部分,后面不显示.这个有点像back face culling, 但是绘制的是line而不是triangle.
想到了3个方法去做这个效果. 第一是用球体+alpha贴图, 第二是用很窄很细的cynlinder来代替圆圈线, 第三种是shader里面处理. 前两种都是画的面片,所以开启back face culling就可以了.试了第二种,效果还可以,就是有时候线过粗,有点变形.
第三种方法是在shader里面clip掉背对相机的点.如何判断一个点是朝向相机还是背对相机? 注意一个点的view space的normal, 正面的点,他们的view space normal都朝向观察点(view space的原点).如下图的viewspace 顶视图:

所以只要clip掉normal.z > 0 或者<0 的像素(根据左右手系)就可以了.注意到sphere/circle通常在local space的原点都是(0,0,0) 那么球面/圈上的点的local space法向量就是normalize(localpos).有了以上分析,就可以写shader了:

 void BladeVSMain(
float4 pos : POSITION,
uniform float4x4 wvp_matrix,
uniform float4x4 wv_matrix,
out float4 outPos : POSITION,
out float3 outViewNormal : TEXCOORD //view space normal
)
{
outPos = mul(pos,wvp_matrix);
float3 normal = pos.xyz;
outViewNormal = normalize( mul(normal, (float3x3)wv_matrix) );
} float4 BladeFSMain(
in float4 pos : POSITION,
in float3 viewNormal : TEXCOORD
) :COLOR0
{
clip( viewNormal.z );
//object_diffuse_color is a built-in semantic for per-instance diffuse
return object_diffuse_color;
}

到这里遇到另外一个问题,是shader变量被优化的问题.本来自动变量WORLD_VIEW_WMATRIX是float4x4, 我在绑定shader参数的时候会做大小和类型检查,但是发现类型不匹配的assertion failure, 一看原来shader里面虽然定义了float4x4,但只用到了world_view的3x3部分(旋转变换), D3DCompile把这个变量的类型也优化成float3x4, 导致跟定义的不匹配, 于是将精确匹配检查改为兼容性检查,只要可兼容,就可以绑定, 这样问题也解决了.

5.helper 的hit test - 根据屏幕坐标得到空间射线(这个很早做的,不多说了),然后根据射线检测出被选中的helper的对应的轴.
问题是箭头尾部的线, 怎么做相交查询. 因为一条线很难被选取,所以要扩大一定范围.本来想直接在投影后的空间做2D相交检测,这样的话,扩大的范围也好计算,但是觉得太麻烦, 最后用的是3D空间的box, 这样box的切面大小就是扩大的范围了. 注意,由于这个box只用于相交检测,不用于渲染,所以不需要创建显卡资源.
rotation helper的轴是三个线圈, 也不好做相交检测, 幸好有一个球在, 根据球面交点的局部坐标,(就知道在哪个轴了)比如交点的localpos.x == 0 说明红色圈(绕x轴的圈)被选中.而这样扩大范围也不难,加上误差比较就可以了.

最后说一下多国语言,这个很早就在考虑,但是没时间做, 但是想了想这个属于很基本的特性, 越到后面越难加, 工作量越大, 索性现在做了.

多国语言原理简单说就是程序中用ID而不用硬编码字符串, 运行时加载语言文件表, 查表得到翻译的字符串.这里面有几个问题需要考虑, 第一,程序员在写代码时,必须有原始语言文件,不断添加资源, 这样很麻烦. 第二,策划在写剧情脚本时, 也要很繁琐的添加文字,然后得到ID, 一样麻烦. 第三,语言文件越来越大可能有无效的信息,需要删除无用的数据. 整个处理流程考虑清楚了,就可以动手了.

所以最初的设想是这样:对于程序员来说, 程序用宏开关, 本地编译时, 使用硬编码的字符串. 编写一个lang-build-tool 来处理程序中出现的字符串,生成内置语言表.  最终发布时, 根据语言查表, 这个语言表不是ID-String map, 而是String List, 就是内置语言(硬编码语言)的列表. 这个表是用来做批量翻译用的,已经与程序源代码编写无关.

根据内置表人工翻译出来的的语言表, 根据在最终发布时一起加载, 组成内置表到目标语言的String-String map.程序在实时根据内置字符串查表就可以得到目标语言.这么做程序员就不用理繁琐的文字资源添加和修改了. 能够这样做前提是程序中自身用到的字符串不多, 而且大都十分短小(每个单元是1-3个单词). 这样效率没有什么大问题.

对于策划来说, 可能字符串的量很大, 而且字符串很长, String-String map的效率可能会低,而且程序已经不需要原始String. 所以做法稍微不同, 任何配置工具都会在内部自动调用语言表的接口, 添加文字到原始语言表, 这个跟程序源代码产生的表类似, 只用于最后批量翻译, 而且与程序生成的内置表不同, 不需要再加载. 翻译后的表是线性String List, (不需要与原始表构成String-String map), 直接用index索引, 而index是多少程序员不需要关心, 因为这个index也存在策划配置文件里面自动生成.

比如程序中的宏如下:

 #define MULTILANG 0

 #ifndef MULTILANG
# define XLang(_str) TEXT(_str)
#else
# define XLang(_str) ILangTableManager::getSingleton().getBuiltInLangString( TEXT(_str) )
#endif

这样程序员只要知道,对于不需要被翻译的字符串,使用TEXT("Something"), 而对于需要被翻译的字符串,使用XLang("Translation"), 内部调试的时候宏开关关闭, 程序员只需要专注于代码就可以了.

同时lang-build-tool 会处理(read-only-parsing) 所有的源代码, 找到XLang("")内部的字符串并添加到内置语言表里, 这个内置语言表用批量作翻译, 和运行时加载.

最终发布时, 将改宏开启, 语言模块会加载 源代码生成的内置表, 和翻译后的目标语言表, 组成map, 供运行时查询, 策划的语言表也会被加载, 被策划配置数据直接使用.

同时, 需要提供一个翻译工具.因为语言表可能是二进制的.即便不是二进制,是纯文本, 也要有对比功能.翻译工具加载原始语言和目标语言的表,对比不同, 提供UI界面给翻译人员做批量翻译.暂时把这个工具叫做lang-diff-edit-tool(这个工具还没有写...) 最终翻译时, 翻译人员需要翻译两张表, 一个是程序源代码被lang-build-tool处理后生成的表, 另一个是各种策划工具生成的一张语言表. 这样批量翻译也没有问题了.

以上只是简单流程, 没有考虑语音和文字图片的因素.
声音资源话,可能要分成普通声音资源(不需要翻译)和语音资源, 文字图片也需要单独分出来, 以便以后批量翻译处理. 最大的问题在于语音和字幕,字幕是需要结合情景来翻译的,而不能简单机械的处理.而且最头痛的是模型动画(面部)口部的动作也要做单独"翻译"处理. 笔者玩过<星际争霸2>的英文版和中文版, 可以看出中文版的角色口型跟英文版的不同, 是配合中文发音的. 不知道这个是美术为每句话专门调的动画呢(-_-!), 还是有工具自动处理的.

理想的情况是有对话语言编辑工具, 将各种基本发音(元音/辅音) 自动作出模型表情动画( 记得OGRE有一个面部表情的例子, 就是根据发音动态做出口型的), 同时需要有语音引擎,将文本转成数字(音标)信息, 用这个信息来处理面部表情.而且,由于一个声音可长可短, 可能仍然不能机械翻译-做动作, 要结合配音演员的声音数据, 分析每个音节的长短, 来做自动动画的时间控制和匹配... 最后,可以加上表情选项, 每句话都可以有不同的表情,这样一个场景对话工具和语言翻译工具才算完美.

上面只是大话一下, 回归现实, 目前我只做了简单的文字翻译... 由于现在没有翻译界面的工具,而我用的是二进制语言表, 所以目前是宏来定义了一堆字符串, 这些信息放在一个或者几个header被lang-build-tool处理, 为了测试语言模块, 写了中文对应的header(这些中文header编译代码时用不到),生成了中文的语言表.现在已经可以动态切换语言了. 这么做对于程序员来说其实也可以, 唯一的缺点就是需要往header里面加字符串和rebuild, 优点翻译是更显性化, 更可控, 也许以后就这么做了.

另外,语言表的字符串,最好用utf8, 这样不用考虑wchar_t的大小(UTF16/UTF32) 或者endian的问题了, 即便是二进制文件, 也用utf8保存单个字符串, 加载时转换为多字节或者wstring.

还有,对于类工厂,如果使用字符串做类创建,那么这个字符串最好不要被翻译, 因为这个类型字符串可能被保存在资源文件里面,  如果切换语言, 一个类被注册成了其他语言字符串, 使用原来资源中的字符串将找不到注册信息. 当然也可以在加载资源的时候,手动将类型信息也翻译了, 不过那么做没太大必要.还有,有部分字符串是在UI模块里面自动翻译的(手动调用接口翻译),特别是属性表的文字. 主要原因是翻译起来不方便, 或者是需要写入资源的属性信息,加载资源时还需要原字符串.

最后贴一张图纪念一下最近的工作, 以后更新仍然会很慢,因为工作很忙, 业余只有周末有点时间.后面会把translate/rotate/scale工具做完,因为现在只是显示了helper,还没有做功能.然后开始搞动画,拖了好久了...

图里面有几个bug待会儿修一下..

引擎设计跟踪(九.8) Gizmo helper实现与多国语言的更多相关文章

  1. 引擎设计跟踪(九.14.2a) 导出插件问题修复和 Tangent Space 裂缝修复

    由于工作很忙, 近半年的业余时间没空搞了, 不过工作马上忙完了, 趁十一有时间修了一些小问题. 这次更新跟骨骼动画无关, 修复了一个之前的, 关于tangent space裂缝的问题: 引擎设计跟踪( ...

  2. 引擎设计跟踪(九.14.2j) TableView工具填坑以及多国语言

    Blade的UI都是预定义的接口, 然后由插件来负责实现, 目前只有MFC的插件. 最近加上了TableView的视图, 用于一些文件的查看和编辑, 比如前面在文件包的笔记中提到需写一个package ...

  3. 引擎设计跟踪(九.14.2 final) Inverse Kinematics: CCD 在Blade中的实现

    因为工作忙, 好久没有记笔记了, 但是有时候发现还得翻以前的笔记去看, 所以还是尽量记下来备忘. 关于IK, 读了一些paper, 觉得之前翻译的那篇, welman的paper (http://gr ...

  4. 引擎设计跟踪(九.14.2i) Android GLES 3.0 完善

    最近把渲染设备对应的GLES的API填上了. 主要有IRenderDevice/IShader/ITexture/IGraphicsResourceManager/IIndexBuffer/IVert ...

  5. 引擎设计跟踪(九.14.2f) 最近更新: OpenGL ES & tools

    之前骨骼动画的IK暂时放一放, 最近在搞GLES的实现. 之前除了GLES没有实现, Android的代码移植已经完毕: [原]跨平台编程注意事项(三): window 到 android 的 移植 ...

  6. 引擎设计跟踪(九.14.2d) [翻译] shader的跨平台方案之2014

    Origin: http://aras-p.info/blog/2014/03/28/cross-platform-shaders-in-2014/ 简译 translation: 作者在2012年写 ...

  7. 引擎设计跟踪(九.9) 文件包系统(Game Package System)

    很早之前,闪现过写文件包系统的想法, 但是觉得还没有到时候. 由于目前工作上在做android ndk开发, 所以业余时间趁热做了android的移植, 因为android ndk提供的mountab ...

  8. 引擎设计跟踪(九.14.3.4) mile stone 2 - model和fbx导入的补漏

    之前milestone2已经做完的工作, 现在趁有时间记下笔记. 1.设计 这里是指兼容3ds max导出/fbx格式转换等等一系列工作的设计. 最开始, Blade的3dsmax导出插件, 全部代码 ...

  9. 引擎设计跟踪(九.14.2g) 将GNUMake集成到Visual Studio

    最近在做纹理压缩工具, 以及数据包的生成. shader编译已经在vs工程里面了, 使用custom build tool, build命令是调用BladeShaderComplier, 并且每个文件 ...

随机推荐

  1. xml文件对应的DTD学习

    DTD文件: 1.DTD文档主要由(元素,属性,实体,PCDATA,CDATA) 2.声明一个元素:<!ELEMENT 元素名称 (元素内容)> eg: <!ELEMENT pers ...

  2. delphi 基础之三 文件流操作

    文件流操作 Delphi操作流文件:什么是流?流,简单来说就是建立在面向对象基础上的一种抽象的处理数据的工具.在流中,定义了一些处理数据的基本操作,如读取数据,写入数据等,程序员是对流进行所有操作的, ...

  3. 《Prism 5.0源码走读》UnityBootstrapper

    UnityBootstrapper (abstract class)继承自Bootstrapper(abstract)类, 在Prism.UnityExtensions.Desktop project ...

  4. Java通过SpyMemcached来缓存数据

    配置好Magent+memcached后,很明显数据之间的输入与输出都是通过代理服务器的,magent是做代理服务器的很明显java在memecached的调用驱动在magent同样适用. 这里选择S ...

  5. 从数组->ArrayList->List 为了方便与安全在不断变化着

    在C#中,当我们想要存储一组对象的时候,就会想到用数组,ArrayList,List这三个对象了. 数组 优点优点之一:数组在内存中是连续存储的,所以它的索引速度是非常的快,而且赋值与修改元素也很简单 ...

  6. linux下更改文件夹所属用户和用户组

    改变所属用户组:chgrp -R users filename -R是为了递归改变文件夹下的文件和文件夹,users是要改为的用户组名称,filename是要改变的文件夹名称 ============ ...

  7. ToolBar存档

    上图是将本阶段要完成的结果画面做了标示,结合下面的描述希望大家能明白. colorPrimaryDark(状态栏底色):在风格 (styles) 或是主题 (themes) 里进行设定. App ba ...

  8. 在windows上使用symfony创建简易的CMS系统(一)

    http://blog.csdn.net/kunshan_shenbin/article/details/7164675 参考自:http://xsymfony.801.cxne.net/forum. ...

  9. 在ASP.NET中实现OAuth2.0(二)之打造自己的API安全策略

    1.场景介绍 公司开发了一款APP产品,前期提供的api接口都是裸奔状态 举个例子:想要获取某一个用户的数据,只需要传递该用户的ID就可以拿走数据(说多了都是泪) 现在想给这些接口穿个衣服,加个壳(对 ...

  10. 66.为什么有时候在ISE软件中,顶层文件不能置顶?

    什么时候回出现顶层文件不能置顶呢?嘿嘿,肯定是工程中有错误啦. 如果你的顶层文件包含了include文件,这个时候就会出现这种情况了.但好像出现在刚新建工程的时候,因为当顶层文件不包括Include文 ...