Shadow mapping 3

先来复习一下ShadowMapping的原理.
首先以灯光的位置渲染场景,这里可以将灯光位置理解为场景的另一部相机.
在开启RTT(渲染到纹理) 的时候,开启深度测试,将深度信息纹理中.成为一张”深度图”
然后我们以正常的相机再渲染一次场景,在渲染的时候顶点着色器依然用正常的相机的投影矩阵输出,但是要把灯光试图投影矩阵转换后的顶点交给像素着色器.
在像素着色器中,将顶点和深度图的像素对齐,对比他们的深度值. 如果当前顶点的深度值大于深度图中的深度值,说明这个顶点肯定被小于它的某个顶点给挡住了,那么该顶点周围的像素都在阴影中. 否则就是在阳光中.

上一篇说到了制作深度图.
把颜色存进32位颜色中
通过
“mul ft0 ft0 fc1n”+
fc1 = 1,255,255*255,255*255*255

嘛~觉得这有点麻烦,毕竟解压的时候还需要将 RGB 反向解码.

换一种方式来搞深度图.

Vertex Program

                //将顶点转入灯光投影空间mvp
                "m44 vt0 va0 vc0n"+
                //输出顶点
                "mov op vt0n"+
                //传入像素着色器
                "mov v0 vt0n");

Fragment Program

                "mov ft0.xyz, v0.zzzn"+
                "mov ft0.w fc0.yn"+
                "div ft0.xyz, ft0.xyz, fc0.xn"+
                "mov oc ft0n");

ft0 即 rgb. 我们将 v0.zzz 赋值给 rgb 既是将深度值存进了rgb中
fc0.y = 1 将 ft0.w = 1 保持像素的透明度总是1
fc0.x = zFar div ft0.xyz ft0.xyz fc0.x 将深度值缩放到投影矩阵的 zFar 之内.
最后将像素输出.

输出后我们会得到如下的深度图. 由于是 0~255的灰度图,嘛~比较不明显,要仔细看
depthMap

继续阅读“Shadow mapping 3”

Shadow Mapping 2

阴影图实现起来确实很多坑等着我…

没有stage3D的资料…是最坑,需要自己摸索

还有几个坑分别是
1.如何创建深度图
2.如何进行深度图采样
3.深度测试
4.AOI3D的封装

今天先从创建深度图开始

首先

            //准备阴影图,并开启为RTT优化
            m_shaderMap = m_context.createTexture(
                1024,
                1024,
                Context3DTextureFormat.BGRA,
                true
            );
           
            /**
             * 阴影shader
             * 将顶点丢入fragmentShader
             */

            var depthPassVertexShader : AGALMiniAssembler = new AGALMiniAssembler();
            depthPassVertexShader.assemble(Context3DProgramType.VERTEX,
                "m44 vt0 va0 vc0n"+
                "mov op vt0n"+
                "mov v0 vt0n");
           
            var depthPassFragmengShader : AGALMiniAssembler = new AGALMiniAssembler();
            depthPassFragmengShader.assemble(Context3DProgramType.FRAGMENT,
                //将深度缩放在 zFar 之内
                "div ft0 v0.z fc0.xn"+
                //将颜色编码为 32 位浮点型数据存入RGBA
                "mul ft0 ft0 fc1n"+
                //取出小数部分
                "frc ft0 ft0n"+
                //255掩码
                "mul ft1 ft0.yzww fc2n"+
                "sub ft0 ft0 ft1n"+
                "mov oc ft0n");

阴影图的大小决定阴影图的精度,越小的阴影图边缘锯齿越严重. 解决方案有很多啦,比如应用模糊滤镜后,再作为查询图.

然后准备我们的shader程序

之后开始渲染,先设置将接下来的模型操作渲染到阴影图.

m_context.clear();
m_context.setRenderToTexture(m_shaderMap,true);

之后开始模型的渲染
准备灯光空间中, 灯光的位置,灯光的相机,灯光的投影

            m_lightModel.identity();
            m_lightModel.appendRotation(-90,Vector3D.X_AXIS);
            m_lightModel.appendRotation(t, Vector3D.Y_AXIS);
            m_lightModel.appendScale(.1,.1,.1);
            m_lightModel.appendTranslation(0,-6,-20);
           
            m_lightView.identity();
            m_lightView.appendTranslation(-50,0,0);
            m_lightView.pointAt(new Vector3D(0,-6,-20),CAM_FACING,CAM_UP);
            m_lightView.invert();

            m_lightProj.identity();
            m_lightProj.append(m_lightModel);
            m_lightProj.append(m_lightView);
            m_lightProj.append(m_projMatrix);

            //vc0 灯光投影矩阵
            m_context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, m_lightProj, true);
            //fc0 m_zFar 最大深度
            m_context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT,0, Vector.<Number>([m_zFar,1,1,1]));
            //1,255,255*255,255*255*255
            m_context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT,1, Vector.<Number>([1,255,65025,16581375]));
            //[(1.0/255.0),(1.0/255.0),(1.0/255.0),0.0]
            m_context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT,2, Vector.<Number>([1/255,1/255,1/255,0]));
           
            //使用shadowMapShader渲染模型
            m_context.setProgram(m_shaderPassShader);
           
            for(var i : int = 0; i < md5Result.meshDataNum; i++){
                var vertexBuffer : VertexBuffer3D = md5Result.vertexBufferList[i];
                var uvBuffer : VertexBuffer3D = md5Result.uvBufferList[i];
                var indexBuffer : IndexBuffer3D = md5Result.indexBufferList[i];
               
                m_context.setVertexBufferAt(0, vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
//              m_context.setVertexBufferAt(1,uvBuffer,0,Context3DVertexBufferFormat.FLOAT_2);
                m_context.drawTriangles(indexBuffer);
            }
           
            //输出到纹理
            m_context.present();

最后是这张阴影图的预览Demo
鼠标中键可控制相机. 原理相机的像素颜色加深~
Demo is here:http://www.dreamfairy.cn/blog/work/flash/3d/stage3dshadowMapping/ShadowMapFinalTest.html

下一篇再实现 深度图查询并渲染~

Shadow Mapping 1

所谓Shadow Mapping 就是阴影投影,又或是阴影图.
这2天绕了好大弯路,发现自己理解错了.今天突然醒悟过来,可以继续开工了.

之前认为Shadow Mapping 是后期效果.
第一次灯光相机视角渲染到纹理来创建一张阴影图
然后在真实相机视角再渲染一张纹理和阴影图进行对比.

实际这是错的,无解! 一张纹理图无法通过矩阵转换到另一张纹理图的坐标系下.

真相实际上是当取得阴影图后,将其作为公用资源对所有允许被光照的模型开放.
所有模型在自己的顶点程序中,现将顶点转换到灯光相机空间,传入像素程序,然后再转换回真实空间进行输出.
以相机空间的坐标进行着色,与阴影图的像素进行对比. 同时计算法线和灯光向量的角度差.

so~ 要继续扩展我的PlaneMesh 类,提供计算法线的方法
阴影图的实现就丢给下一篇文章~

Aoi3D Alpha

花了2天~把 AOI3D 引擎? 的 Alpha 版本撸出来了. so~ 今天是个直接纪念的日子,不管未来AOI3D会不会搁浅,总之是起了个头.
材质目前就实现2只. 纯色材质 ColorMaterial 和 纹理材质 TextureMaterial

多边形目前只有 Plane. 特别喜欢 Flare3D 的 Plane 的API. 于是山寨了过来, 可以定义 +xy,+xz,+yz 平面,以及顶点分割.

明天如果有空的话,把MD5模型 基于 MeshBase 加入到引擎中. 仅仅是无动画的纯Mesh
后天如果有空的话,把Shadow Map 加入引擎,并构建后期处理队列.

今天项目经理开会,貌似下周手头的项目要进入里程碑了,大概会很忙吧~ 希望能如期开始粒子方面的研究啊~~
也许周末都要拿来写代码的(最好不要)

AOI3D ALPHA 的 GIT-HUB: https://github.com/dreamfairy/3DDemo/blob/master/StencilShadow/src/ShadowMap.as

之后所有的功能都会在引擎的基础上搞,嘛~起步可能会艰难点,不过以后扩展开发就轻松了啊.

[美国末日]入手啦~

今天顺风终于把我的<美国末日>寄到了,贴2张图. 嘿嘿~
tlou1

tlou2

嘛~玩了好久,终于看到pv里的小萝莉了.

要找个夜深人静的夜晚,一气呵成,才有感觉.

逐像素光照渲染

逐像素的渲染的精度比顶点的要高,效果也要好.
一般做像素渲染最好的做法就是准备一张法线贴图.

说到法线贴图,实际上就是凹凸贴图. 其设计的目的是为了在简模(顶点数低)上实现复杂的凹凸效果. 而凹凸效果实际上是通过虚假的阴影绘制后给人眼造成的立体假象.
本例使用的是 Doom3 模型的MD5模型,其附带的法线贴图是3通道贴图,即 r,g,b 分别表示法线的 x,y,z.
实际上为了节省硬碟空间,还有2通道贴图. 不过貌似现在大家显存和内存都很足的说~

如果将法线贴图丢入shader. 可以直接用float4进行采样,很容易就取出r,g,b 分量.
如果想用CPU计算Normal的话,用如下方式

//先取出颜色
var color : uint = bmd.getPixel(x,y);
//然后取出分量
var red : Number = ((color & 0xFF0000) >> 16)/256;
var green : Number = ((color & 0x00FF00) >> 8)/256;
var blue : Number = ((color & 0x0000FF)/256;

接下来shader就容易多了
将法线于灯光进行点乘
然后取出大于0的部分
对于这部分进行纹理混合即可.

doom3 法线贴图

doom3 漫反射贴图

fragmentShader Code

fragmentShader = new AGALMiniAssembler();
            fragmentShader.assemble(Context3DProgramType.FRAGMENT,
                //纹理采样
                "tex ft0, v0, fs0<2d, linear, repeat>n" +
                "tex ft1, v0, fs1<2d, linear, repeat>n" +
                //灯光点乘法线
                "dp3 ft2, ft1, fc2n" +
                "neg ft2, ft2n" +
                "sat ft2, ft2n"+
               
                //混合环境光
                "mul ft3, ft0, ft2n" +
                //混合灯光颜色
                "mul ft3, ft3, fc1n" +
                //输出
                "add oc, ft3, fc0");

ft0 是采样的是模型贴图
ft1 采样的就是法线贴图

直接取出法线贴图,进行灯光的dp3点乘即可。

GitHub : https://github.com/dreamfairy/3DDemo/blob/master/StencilShadow/src/MD5Test.as

Demo 地址: http://www.dreamfairy.cn/blog/work/flash/3d/stage3DNormalTex/md5test.html

啊~Demo中的灯光使用Cube做的,做完才觉得好难看~~ 下次用Sphere 来做吧!
原版计划开搞引擎的,嘛~突然又对shadow map 感兴趣了。 嘛~ 还是再挑战一下自己吧!

逐顶点光照渲染

逐顶点光照渲染, 实际上就是根据顶点算出当前顶点的法线. 然后将该法线传入像素着色器中. 因为是逐顶点的,顶点之间的距离有时相对较大,因此当把法线传给像素程序的时候,顶点间的阴影是差值计算的.

一般在顶点shader中计算法线的话,都需要正方体或球体, 且中心点在绝对中心,这样算的法线比较准确. 只需要Normalize 各顶点即可. 如果是不规则的多边形,会算不准的说.

github:Source Code

在写这篇文章的时候,还参考了这篇博客: http://blog.norbz.net/2012/04/stage3d-agal-from-scratch-part-vii-let-there-be-light/
里面有很详细的讲解,不过貌似国内无法访问的样子~ 骚年们,翻墙吧!

最后Demo is here
http://www.dreamfairy.cn/blog/work/flash/3d/stage3dlight/

要有光!

按计划,最近把光线搞完之后,就开始试着写3D引擎了。
虽说“每月一游戏”系列坑掉了,但也不枉费我几个月猛啃书籍的努力。在所开头的几个游戏也是借用别人的引擎。
Hell! It’s about time!

端午节玩了好多游戏~ <勿忘我> 通关了! <神秘海域2> 开坑! <暴雨> 到货! <美国末日> 发货!
玩过瘾先~ Demo什么的都是浮云!

[勿忘我] 通关啦! 剧情还是很赞的.就是打斗多到有点繁琐了. 老妈和老爸居然是最终BOSS. 还洗白了. 义军老大一开始就觉得他是坏人,居然结局是个自我牺牲的英雄! 三观尽毁!

让我们的MD5模型动起来,以及CPU渲染那些事~

md5Anim文件中各字段,我的理解是
hierarchy 是层级关系
数据内容为: 骨骼名称,父级索引,flag, 索引起始位
其中flag是一个用来做二进制求与关系来决定当前的数据是 偏移Tx,Ty,TZ 还是 旋转Qx,Qy,Qz
索引起始位是 frame 动画帧字段中数据的起始索引

bounds 是包围盒
通过 min 和 max 可以决定当前动作的 AABB. 不做碰撞检测的话,一般用不到

baseframe 基础帧信息
实际上模型的各骨骼动画都是以此为起始值进行偏移的
6个值分别是 Tx,Ty,Tz,Qx,Qy,Qz

frame 具体的动画帧信息
数据内容为: 帧索引, numAnimatedComponents 定义的长度数量的 动画数据。 其中的数值具体有什么用,需要查询 hierarchy 中的 flag.

具体转换骨骼动画的代码如下

private function CalcMeshAnim(frameData : MD5FrameData) : void
        {
            //取出关节数据
            var joints : Vector.<md5joint> = md5MeshParser.md5_joint;
            var jointsNum : int = joints.length;
           
            cpuAnimMatrix ||= new Vector.<matrix3d>(jointsNum * 4, true);
           
            var joint : MD5Joint;
            var parentJoint : MD5Joint;
            for(var i : int = 0; i < jointsNum; i++)
            {
                //从基本帧开始偏移
                var baseFrame : MD5BaseFrameData = md5AnimParser.baseFrameData[i];
                var animatedPos : Vector3D = baseFrame.position;
                var animatedOrient : Quaternion = baseFrame.orientation;
               
                //将帧数据替换掉基本帧中对应的数据
                var hierachy : MD5HierarchyData = md5AnimParser.hierarchy[i];
               
                var flags : int = hierachy.flags;
                var j : int = 0;
                if(flags & 1) //tx
                    animatedPos.x = frameData.components[hierachy.startIndex + j++];
               
                if(flags & 2) //ty
                    animatedPos.y = frameData.components[hierachy.startIndex + j++];
               
                if(flags & 4)
                    animatedPos.z = frameData.components[hierachy.startIndex + j++];
               
                if(flags & 8)
                    animatedOrient.x = frameData.components[hierachy.startIndex + j++];
               
                if(flags & 16)
                    animatedOrient.y = frameData.components[hierachy.startIndex + j++];
               
                if(flags & 32)
                    animatedOrient.z = frameData.components[hierachy.startIndex + j++];
               
                //计算w
                var t : Number = 1 - animatedOrient.x * animatedOrient.x - animatedOrient.y * animatedOrient.y -
                    animatedOrient.z * animatedOrient.z;
                animatedOrient.w = t < 0 ? 0 : - Math.sqrt(t);
               
                var matrix3D : Matrix3D = animatedOrient.toMatrix3D();
                matrix3D.appendTranslation(animatedPos.x, animatedPos.y, animatedPos.z);
               
                //取出当前关节
                joint = joints[i];
               
                if(joint.parentIndex < 0){
                    joint.bindPose = matrix3D;
                }else{
                    //如果该关节有父级,需要先附带上父级的旋转和偏移
                    parentJoint = joints[joint.parentIndex];
                    matrix3D.append(parentJoint.bindPose);
                    joint.bindPose = matrix3D;
                }
               
                matrix3D = joint.inverseBindPose.clone();
                matrix3D.append(joint.bindPose);
               
                var vc : int = i * 4;
                if(useCPU){
                    cpuAnimMatrix[vc] = matrix3D;
                }else{
                    context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, vc, matrix3D, true);
                }
            }

继续阅读“让我们的MD5模型动起来,以及CPU渲染那些事~”

MD5模型的解析器

虽然之前看了相关MD5的文件结构资料,自认为实现解析应该没问题,嘛~毕竟是停留在理论, 还是动手来实践一下。
之后看到某同学写了个MD5的解析Demo,于是研究了下发现,这实际上是个 Away3D移植出来的精简解析程序,只支持单个Mesh. 近期把它改进成支持多个Mesh吧!

网络上有很多人都发过MD5的文件格式讲解,嘛~我觉得按自己的思路整理一下也发一次。 just for myself;

numJoints 110 //总关节数
numMeshes 4 //总网格数

joints {} 关节节点,其数据顺序为
关节名称,关节父级ID,关节位置,关节旋转

关节在对无动画的纯md5Mesh模型来说没有意义,渲染时无需用到。

mesh {} 网格节点,其数据顺序为
vert token
顶点索引,顶点纹理U,顶点纹理V,顶点初始权重,顶点最大权重

tri token
三角形索引,构成三角形对应的三个顶点的索引

weight token
权重索引,权重对应的关节索引,权重值,权重位置

如果有多个网格,就是以上数据的重复。

之后是精简版解析器的源码,附带的中文的注释。 这几天看看把它扩展成多网格的版本。

继续阅读“MD5模型的解析器”