1. Input& Native-Activity
首先,先提一下Android的版本兼容。Android的兼容理论上很简单,Android有不同的Api level(比如2.2(8)是较早出现,4.2(17)是最近发布的)。一个运行的Android OS 是 2.3.4 (10)的手机可以跑10或是10以下的API写的程序,即是说即使你的程序支持的OS最高为8,在8以上ie. 9、10、11 ect都能跑。
支持的api level决定于你本机开发环境(AndroidSDK),以及在Android工程里Manifest的设置。
一个问题大多开发者都会碰到:Android的开放性决定了兼容性比ios难做(是难很多,原因大家都明白的。。。各个机型的OS源码都可能被更改过,而且Android OS的更新也很积极。)
然后说一下当前个人关于编写Input模块时遇到的问题:
相机的操作中需要获取用户的触屏输入信息。几个手势分别映射不同的事件。轻点(Tap)事件用于点击物体。手指滑动(Drag/Flip)用于旋转或移动物体(或相机)、双指并拢(Pinch)或拉开(Spread)用来缩放物体。
在使用Android的Java的手势识别Api完成以上操作的代码后,发现程序在2.2.1(8)和2.3.4(10)的手机上运行正常,在Android 4.0.4(15)的机器上却出现难以相应手势输入的情况。虽然4.0.4的手机配置是4核,帧数很高,但是对手指输入的反应却是点击十几次才能相应一次。试着使用Sleep让出点CPU时间,但是没有改善。只能再想其他办法。
上网一查,别人也有碰到类似4.0上手势识别的bug问题,原因是对旧sdk兼容似乎不大好。继续看了一下ndk的sample,发现ndk除了使用Jni混写C++和Java的方法外,还有纯C++的写法,即把Java的Activity隐藏到了后台的Native-Activity。Android Api在Native-Activity的实现中,手势、Assets获取等都提供了C++的接口,功能等同原本Java接口。不过Native-Activity在android-9才开始支持。即是说系统必须是2.3即2.3之后的手机才能运行。
几乎没怎么犹豫,就着手把驱动层全改为Native-Activity实现,把原来就不适应的Java代码都去掉了。除了2.2.1的手机跑不了之外(果然8不行),2.3.4和4.0.4都正常,而且exciting的是4.0.4上的输入的bug真的就这样没了,改用C++接口AInputEvent的程序的输入响应十分顺畅。
2. AndroidSDK 4.0升级和新的ADT
看到4.0之后SDK的模拟器可以运行Gles2.0还有GPU加速,有些被吸引。
就去下载SDK升级到了4.2(Apilevel 17)。
果然之前3.x的SDK跑不了的Gles2.0,现在的模拟器能跑。感觉系统响应快了些,Gles2.0的shader和RTT等都支持,不过GPU效率还觉得慢。同时提一下,新的ADT比原来集成度高,Android这次提供了一个已经配置好的eclipse环境(ADT)供下载。
http://developer.android.com/sdk/index.html
在以上Android官网上找到 “Downloadthe SDK ADT bundle for Windows”下载并安装,只要下载并安装这个,等于安装并设置了eclipse,CDT和ADT,省去以前挨个下载、手工设置的操作。有兴趣可以下载体验一下。
3. 回到图形,完成前一章说过的有些要做的模块。
边放Demo图边解释。。
DEMO2a Camera_Input
这个模块要点有两个:系统输入处理和事件解析。
系统输入处理:
- 在Windows模拟器这边直接使用WinApi处理鼠标事件。点击、鼠标移动和中轮滑动控制物件的选取、转动和缩放。
- Android平台上手势(手指触屏操作)获取如本文第一节说使用了ndk 的AInputEvent(C++接口)等实现。Android支持多点触控,需要跟踪一个以上的touchpoint。
事件解析:
- 点击、鼠标(指尖)移动都比较容易处理。
- 相应手势的Drag转动物体参考了nehe的ArcBall(轨迹球)的实现。实际上D3D9的DXUT里的相机也是轨迹球,但是nehe的sample更容易理解,假设物体在一球内,把点击坐标映射到球面上,转动球也就转动了物体。构造围绕转动轴转动一定角度的4元数,再把4元数转换为转动球的矩阵。
- 在Android真机上两指可以做缩放操作,这个需要计算前一帧和当前的两指之间距离的比值。Windows则直接获取鼠标滑轮的滑动位移换算为缩放值。
为了尽量使上层代码平台无关,App逻辑层不直接访问OS事件Api,而是获取解析后的事件(即是否点击、缩放、转动而非手指或鼠标状态)。
DEMO2b Sprite
- Sprite封装二维对象绘制。
- 在Sprite上加上Font,并判断点击相关区域即可以实现简单的Button。
- 这里有snake(匍匐的蛇)和star(转动的星星)两套2维骨骼动画来测试图片的移动旋转缩放及绘制。点击屏幕左方按钮可以切换。
DEMO2c SkinnedMesh
着手Skinnedmesh时首先要解决的问题就是如何获取资源,就是说是导入已有的骨骼动画的格式还是自定义格式。开源的游戏引擎或是FBXSDK都相对较复杂,最后还是决定使用milkShape,因为milkShape格式的导入代码是公开的,而且很精短十分容易理解,改起来很方便J
于是在原来的模型导出器上加入载入.ms3d(milkShape格式)模型的功能,导出自定义的带有骨骼动画信息的Mesh文件。Demo图中的手脚会动的兔子和乌龟模型是自己在milkShape里做的(那个会弯的圆柱是MilkShape代码Sample里自带的)。
SkinnedMesh使用到的数据结构定义如下:
struct sKeyframe { floattime; Vec3key; }; struct sJointTangent { Vec3tangentIn; Vec3tangentOut; }; struct sJoint { unsignedchar flags; charname[32]; charparentName[32]; Vec3rot; Vec3pos; floatcolor[3]; std::vector<sKeyframe>rotationKeys; std::vector<sKeyframe>positionKeys; std::vector<sJointTangent>tangents; //used for rendering intparentIndex; Mat4x4matLocalSkeleton; //Mat4x3is enough indeed.. Mat4x4matGlobalSkeleton; Mat4x4matLocal; Mat4x4matGlobal; }; //!-- max 32 matrices of bone in shader #define MAX_HARDWARE_MATRIX_NUM 32 class ZTSkinnedMesh : public ZTMesh { protected: std::vector<sJoint>mJointList; floatmfCurrentTime; floatmfTotalTime; bool mUseSoftVP; //softwarevertex process float**mArSoftVPBuf; // vertex data for SoftVP Mat4x4*mArCombinedBoneMatrix; //combined bonematrix array ….省略方法 }
Joint结构和骨骼动画更新的算法基本都抄MilkShape的,绘制就需要自己重写了。在支持软件混合(blending)的同时,也支持硬件的顶点混合。实际运行结果也证明不论是Windows的Gles2模拟器还是Android手机上,硬件混合要快很多。VertexShader里全局的骨骼矩阵数量设为32个,即上限支持32个骨骼。每帧由CPU更新好的骨骼矩阵(combined bone matrixarray),传入GPU做顶点混合。下面是VertexShader的代码。boneIndex = boneIndex.yzwx;这样的”Rotate”技术可以避免对向量的[]索引操作。
attribute highp vec3 v_Vertex;
attribute mediump vec3 v_Normal;attribute mediump vec2 v_UV;
attribute mediump vec4 v_BoneId;
attribute mediump vec4 v_BoneWeight;
uniform mediump mat4 u_matMPV;
uniform mediump mat3 u_matModelView;
uniform mediump mat4 u_BoneMatrixArray[32];
uniform mediump int u_BoneCount;
uniform mediump vec3 u_vLightDirection;
varying mediump float varDot;
varying mediump vec2 varCoord;
void main(void)
{
mediumpvec3 normal;
if(u_BoneCount> 0)
{
mediumpivec4 boneIndex = ivec4(v_BoneId);
mediumpvec4 boneWeights = v_BoneWeight;
floatweightSum = boneWeights.x;
mediumpmat4 boneMatrix = u_BoneMatrixArray[boneIndex.x];
highpvec4 position = boneMatrix * vec4(v_Vertex, 1.0) * boneWeights.x;
mediumpvec3 blendNormal = (boneMatrix * vec4(v_Normal, 0.0)).xyz * boneWeights.x;
for(lowp int i = 1; i < 4; ++i)
{
if(i< u_BoneCount && weightSum < 1.0)
{
//<-rotate<- the vector components
boneIndex= boneIndex.yzwx;
boneWeights= boneWeights.yzwx;
boneMatrix= u_BoneMatrixArray[boneIndex.x];
position+= boneMatrix * vec4(v_Vertex, 1.0) * boneWeights.x;
blendNormal+= (boneMatrix * vec4(v_Normal, 0.0)).xyz * boneWeights.x;
weightSum+= boneWeights.x;
}
}
gl_Position= u_matMPV * position;
normal= blendNormal;
}
else
{
gl_Position= u_matMPV * vec4(v_Vertex, 1.0);
normal= v_Normal;
}
normal= normalize(normal);
varDot= max( dot(normal, u_vLightDirection), 0.0 );
varCoord= v_UV.st;
}
这里有一个遇到印象比较深的bug。BoneId的数据类型一开始我想当然的定义为了int[4],shader里的attribute变量也用int型的ivec4,于是导致了硬件混合时什么都画不出。最后查出来是Gles2的顶点数据须声明为float类型,在shader里索引矩阵(即骨骼Id)时转为ivec4。
DEMO2d RenderToTexture
Gles2的RTT实现需要建立FrameBufferObject(FBO),FBO负责管理绘制目标(RenderTarget),切换FBO就好像把纹理指定为当前窗口,当当前的绘制目标切为你自定义的FBO时,你就可以往纹理绘制任何内容了,而且绘制代码和在普通绘制到屏幕代码完全相同不需任何更改。 Demo里有一个主相机还有个副相机,一个绘制角色到屏幕,另一个绘制到纹理(然后画在了屏幕左下角)。另外,还有个用于控制灯光(用黄色球形表示)的相机,就像D3D的DXUT里Sample一样,相机的移动旋转控制也可用于控制灯源的位置方向。绘制到纹理(RTT)实现Gles2和Gl2代码几乎完全相同(除了纹理像素格式限制比Gl2严格,比如没有看到Gl2中可以使用的RGBA4通道都为32位浮点的RTT纹理)。 下面有供参考的创建FBO、切换FBO代码。PINGPONG指需要做后处理、即再次送入shader做像素处理的情况,这种处理需要2个可以切换的FBO,每个FBO各包含1套纹理组,可以实现钩边、HDR等效果。绘制到深度做了特别处理,让深度纹理可以直接被使用。深度纹理精度一般是16,也可是32位的,代码里用宏区分。 glFramebufferTexture2D 用于绑定纹理于当前FBO,第二个参数可选GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1,…是和后备缓存(或称为targetbuffer)一样格式(一般为RGBA)的和FBO绑定的纹理,一个FBO可以绑定多个渲染纹理(MRT-Multi-RenderToTexture),这样绘制一次可以渲染到多张纹理(可以把颜色ATTACHMENT0、法线同时输出到ATTACHMENT1,做延迟渲染,也可应用于一些通用计算)。FBO同时可以绑定深度纹理,此时不要用GL_COLOR_ATTACHMENTn,而是用GL_DEPTH_ATTACHMENT。
bool ZTPostProcess::CreateFBOAndTextures() { // Get the currently bound frame buffer object. On most platforms this just gives 0. glGetIntegerv(GL_FRAMEBUFFER_BINDING, &miOriginalFbo); int numFboNeed = 0; switch(meRttType) { case RTT_RGB: case RTT_RGB_DEPTH: numFboNeed = 1; break; case RTT_RGB_PINGPONG: numFboNeed = 2; break; } if(RTT_RGB_DEPTH != meRttType) { glGenRenderbuffers(1, &muiDepthRenderbuffer); // bind renderbuffer and create a 16-bit depth buffer glBindRenderbuffer(GL_RENDERBUFFER, muiDepthRenderbuffer); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, muiTexWidth, muiTexHeight); } for(int i = 0; i < numFboNeed; i++) { // generate 1 framebuffer, renderbuffer, and texture object names glGenFramebuffers(1, &muiFBO[i]); glGenTextures(1, &muiTextures[ATTACH_COLOR_TEXTURE_0 + i]); // bind texture and load the texture mip-level 0 // texels are RGB565 glBindTexture(GL_TEXTURE_2D, muiTextures[ATTACH_COLOR_TEXTURE_0 + i]); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, muiTexWidth, muiTexHeight, 0, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); if(RTT_RGB_DEPTH != meRttType) { // bind the framebuffer glBindFramebuffer(GL_FRAMEBUFFER, muiFBO[i]); // specify texture as color attachment glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, muiTextures[ATTACH_COLOR_TEXTURE_0], 0); // specify depth_renderbufer as depth attachment glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, muiDepthRenderbuffer); } else { // bind depth texture and load the texture mip-level 0 glGenTextures(1, &muiTextures[ATTACH_DEPTH_TEXTURE]); glBindTexture(GL_TEXTURE_2D, muiTextures[ATTACH_DEPTH_TEXTURE]); #ifdef USE_GL_DEPTH_COMPONENT16 glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, muiTexWidth, muiTexHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, NULL); #else glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32_OES, muiTexWidth, muiTexHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL); #endif glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // bind the framebuffer glBindFramebuffer(GL_FRAMEBUFFER, muiFBO[i]); // specify texture as color attachment glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, muiTextures[ATTACH_COLOR_TEXTURE_0 + i], 0); // specify texture as depth attachment glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, muiTextures[ATTACH_DEPTH_TEXTURE], 0); } } // check for framebuffer complete int status = glCheckFramebufferStatus(GL_FRAMEBUFFER); if(status != GL_FRAMEBUFFER_COMPLETE) { LOGE("cannot use framebuffer objects, creation of render buffer falled"); return false; } glBindFramebuffer(GL_FRAMEBUFFER, miOriginalFbo); return true; } void ZTPostProcess::StartRenderToTexture() { //old fbo, usually 0 glGetIntegerv(GL_FRAMEBUFFER_BINDING, &miOriginalFbo); //old clear color glGetFloatv(GL_COLOR_CLEAR_VALUE, miOriginalClearColor); // render to texture using FBO glBindFramebuffer(GL_FRAMEBUFFER, muiFBO[miCurrentBindFboInd]); // clear color and depth buffer glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } void ZTPostProcess::EndRenderToTexture() { //restore to old fbo, usually 0 glBindFramebuffer(GL_FRAMEBUFFER, miOriginalFbo); //restore clear color glClearColor(miOriginalClearColor[0], miOriginalClearColor[1], miOriginalClearColor[2], miOriginalClearColor[3]); }
![](https://img-my.csdn.net/uploads/201303/01/1362134187_9635.png)
DEMO2e ShadowMap
阴影图使用的是Z缓冲器算法,从投射阴影的光源位置处对整个场景进行绘制。对于Z缓冲器内的每一个像素,它的Z深度值包括了这个像素到距离光源最近的物体的距离。一般将Z缓冲器中的整个内容称为阴影图(shadowmap),有时候也称为阴影深度图或者阴影缓冲器。 为了使用阴影图,需要将场景进行二次绘制。第一次绘制阴影图,存放场景物体到灯源的最近深度。第二次是从视点的角度来进行:对每个图元进行绘制的时候,将他们的位置与阴影图进行比较,如果绘制点距离光源比阴影图中的数值还要远,那么这个点就在阴影中,否则就不在阴影中。见下图。
如图中,左边表示将点的深度值保存在可见的物体表面上,形成一个阴影图。右图中眼睛正在观察的两个位置,点Va,Vb,分别对应阴影图上的点a,b。Va处到光源的距离不大于阴影图a中的保存的数值,所以Va被照亮。点Vb到光源的距离大于阴影图中b点保存的深度值,所以点Vb在阴影中。 这种技术可以通过纹理贴图硬件来实现。场景中每个光源都相当于一个独立的摄像机,都有个一个独立的阴影贴图。首先在光源的位置设定相机,绘制场景,生成阴影图。此时只需要Z缓冲器操作,也就是说这个时候可以不计算光照,也不需纹理映射,因为对于颜色缓冲器不需写入,而只是记录Z值。然后从观察者角度使用环境光来对整个场景进行绘制,对可见像素执行阴影测试,执行光照、阴影着色。
阴影测试步骤:
回想一下绘制流水线中顶点变化部分:在统一的世界坐标下的顶点都会经过观察(摄像机)矩阵和投影矩阵变化到视域近平面的投影空间。现在我们需要做的就是在不同的相机空间之间的转化变化:1)施加灯光的摄像机矩阵,和灯光投影矩阵,最后做灯光的投影空间到纹理空间的变换。结果把物体变换到光源阴影贴图坐标系。2)现在顶点处于光源阴影贴图坐标系中了,且经过了在制作阴影贴图时同样的深度映射,把其深度值和阴影贴图中的值比较就能知道他是否存在于阴影中了。
阴影图算法不足之处就是阴影质量与阴影图的分辨率有关,同时也依赖Z缓冲器的数值精度。由于阴影映射技术基于图像,使用离散采样,分辨率有限,而实际场景中物体尺寸和距离各不相同,所以不可避免的会产生锯齿走样问题。当场景非常大时、观察者接近阴影就会见到明显的锯齿走样与拉伸。一种缓解方法是利用像素着色器对阴影图进行多点采样并插值。以阴影图中距离样本位置最近的若干个纹理像素的深度值来与最终要绘制约像素距光源的深度值进行比较来判断一个点是否落在阴影中。 阴影图算法的优点是可以用一般的图像硬件对任意阴影进行绘制,而且创建阴影图的代价与需要绘制的图元数量成线性关系,访问阴影图的时间也固定不变。此外,只要光源位置和者物体位置没有发生变化,那么一个阴影图就可以用于多帧画面。因为阴影与视点无关,所以允许视点位置变化。相反在处理动态光源/物体的时候,更新每帧时阴影图都要重新绘制。
有了前一节绘制到纹理的铺垫,实现ShadowMap就很方便了。Demo中把相机设于灯光所在位置与朝向、绘制场景到颜色纹理与深度纹理。深度纹理就是制作阴影所需的ShadowMap。
DEMO2f CartoonEffect
想稍微试着做个特效的Demo,就有了这个、最简单的卡通渲染。 顶点Shader是最基本的TnL,着色用Shader里只有一句:
gl_FragColor.rgb = texture2D(samplerMesh,varCoord).rgb *
(texture2D(samplerToon,vec2(varDot, 0.5)).rgb * u_vDiffuse + u_vAmbient) + u_vEmissive;
原理就是离散光照的强度表现,按法线和光线的点积(0~1)作为纹理坐标取纹理颜色。以下就是这个离散光照用的纹理,做成一维纹理也可以,不过二维的较易实现。
Demo是不带钩边的实现,带钩边的最简单的实现是求法线和视线的夹角,夹角较大接近于90度的就是处在‘边’的位置,这个算法可以使用很简单的Shader实现。稍复杂的可以参考Nehe的教程里有个利用Z缓冲做钩边的Demo,另外在“3D游戏程序设计入门”里(Dx9)的实现是计算相邻边的法线夹角、这个算法不适合Shader中无法读取邻边信息的Gles2。
下载了3个Google Sketch的模型,直接下下来的模型法线没正确处理,一些三角形法线方向是反的,法线反了即三角形索引顺序倒了,这样不仅光照表现会出问题,在背面剔除时三角形也会被剔除掉。画了点时间矫正法线(转为.obj后导入建模工具矫正法线),再导为框架的自定义格式,还好3个模型画出来,最后的效果还过得去。 这样第二阶段Demo就此告一段落,能看到这里也够耐心了。
作为练习这个框架已经可以算完了,从起步到现在也有5个多月了。之后由于要在学校里写论文,可能会利用这个框架做一些基于物理的模拟或渲染。初步计划做一个动态头发的实时模拟,可能要会花几个月,实验成果的话再想办法更新博客吧微笑