让我们来创建后期效果-高动态光照渲染!

先放2张图
第1张是没有HDR效果的。
cryb

第2张是HDR效果的
crya

这个效果原本是GLSL的Shader. 我先移植到 Pixel Bender 上验证自己的理解没有错后,再移植到AGAL上,最后和之前的Blur效果融合。

这是Pixel Bender 的代码

<languageversion : 1.0;>

kernel Lum
< namespace : "cn.dreamfairy";
    vendor : "dreamfairy";
    version : 1;
>
{
    input image4 baseTex;
    input image4 bloomTex;
    output pixel4 dsts;
   
    //外部参数
   parameter float fAvgLum
    < minValue:float(0.1);
        maxValue:float(0.5);
        defaultValue:float(0.5);
    >;

    parameter float fDimmer
    < minValue:float(0);
        maxValue:float(1);
        defaultValue:float(1);
    >;

    void
    evaluatePixel()
    {
        float4 texCol = sampleNearest(baseTex,outCoord());
        //计算平均亮度
        float vLum = 0.27 * texCol.r + 0.67 * texCol.g + 0.06 * texCol.b;
        //亮度缩放
        float vLumScaled = fDimmer * vLum / fAvgLum;
       
        texCol = float4(texCol.r * vLumScaled, texCol.g * vLumScaled,
        texCol.b * vLumScaled, texCol.a * vLumScaled);
        texCol = texCol / (float4(1.0) + texCol);
       
        float4 texBloom = sampleNearest(bloomTex, outCoord());        
        dsts = texBloom + texCol;
    }
}

继续阅读“让我们来创建后期效果-高动态光照渲染!”

让我们来创建后期效果-模糊!

原计划是丢HDR效果的,但是实际上HDR效果中要用到Blur效果的,因此先丢Blur

模糊效果是后期效果中比较简单效果. 这里不做径向模糊,不做正态分布,纯粹的模糊就是将当前像素的颜色以周围的像素为平均值来求出.
根据这个原理,当拿到uv值时,只需要做一次3*3的遍历,来求出周围8个像素,然后将像素叠加/8即可获得平均值,剩下的事就是用AGAL来求出了.

不上图了,做个代码笔记,之后就是Demo

周围像素的数量为圈数 n – 1 * 3 * 24, 其中 n > 1; 当 n 小于等于 1时. 像素的数量为8
整理为代码就是
var index : uint = step <= 1 ? 1 : (step - 1) * 3; var num : uint = 8 * index; 之后我们需要用拼接的方式来构造AGAL

        private function getBlurAgal(step : uint, force : uint, coord : String, pTempIndex : int, pSaveIndex : int, pConstantOffset : int, pTextureIndex : int, constant : Vector.<Number>, pInitIndex : int) : String
        {
            var data : Array = new Array();
            var isFirstSave : Boolean = true;
            var loopNum : uint = 0;
            var stepX : Number = force / stage.stageWidth;
            var stepY : Number = force / stage.stageHeight;
            var index : uint = step <= 1 ? 1 : (step - 1) * 3;
            var num : uint = 8 * index;
            var tempIndex : int = pTempIndex;
            var saveIndex : int = pSaveIndex;
            var textureIndex : int = pTextureIndex;
            var initIndex : int = pInitIndex;
            var constantOffset : int = pConstantOffset;

            constant = constant || new Vector.<Number>();
            constant.length = 0;
           
            data.push("mov ft" + tempIndex + ", fc" + initIndex);
           
            //横向遍历
            for(var i : int = -step; i <= step; i++)
            {
                //纵向遍历
                for(var j : int = -step; j <= step; j ++)
                {
                    //跳过中心区
                    if(i == 0 && j == 0) continue;
                    constant.push(i * stepX, j * stepY);
                    data.push("add ft" + tempIndex + ".xy, " + coord + ".xy, fc" + constantOffset + (loopNum % 2 == 0 ? ".xy" : ".zw"));
                    data.push("tex ft" + tempIndex + ", ft" + tempIndex + ", fs" + textureIndex + "<2d, repeat, linear, mipnone>");
                    data.push(
                        isFirstSave ? "mov ft" + saveIndex + ", ft" + tempIndex :
                        "add ft" + saveIndex + ", ft" + saveIndex + ", ft" + tempIndex);
                    isFirstSave = false;
                    if(++loopNum % 2 == 0) constantOffset++;
                }
            }
           
            data.push("div ft" + saveIndex + ".rgb, ft" + saveIndex + ".rgb, fc" + constantOffset + ".w");
            data.push("mov oc, ft" + saveIndex);
           
            if(constant.length / 4 > 28){
                return getBlurAgal(1,1,coord,pTempIndex,pSaveIndex,pConstantOffset,pTextureIndex,constant,pInitIndex);
            }
           
            constant.push(0,0,0,num);
            return data.join("n");
        }

var stepX : Number = force / stage.stageWidth;
var stepY : Number = force / stage.stageHeight;
求出当前屏幕大小纹理的u和v的步进值

data.push(“mov ft” + tempIndex + “, fc” + initIndex);
初始化用来保存周围像素的值的寄存器, 初始化值这里是 0,0,0,0

(loopNum % 2 == 0 ? “.xy” : “.zw”)
由于常量寄存器数量只有28个,因此如果每个float4 我们只有xy,那么zw就被浪费了,Blur的层级也无法提升,因此基于物尽其用的原则,每2个uv使用一个float4.

if(constant.length / 4 > 28)
由于常量寄存器只有28个, 其中每4个长度为一个 float4 因此当超过28个寄存器时,将模糊滤镜重置为最小滤镜

data.push(
isFirstSave ? “mov ft” + saveIndex + “, ft” + tempIndex :
“add ft” + saveIndex + “, ft” + saveIndex + “, ft” + tempIndex);
isFirstSave = false;
用来记录当前累加颜色的寄存器必须初始化,可以给它丢个initIndex中的值,但实际上这里直接把第一次的得出的颜色值mov进去就当作它的初始化值就够了,不需要额外的初始化.

使用方法:
var m_blurAgal : String = getBlurAgal(3, 1, “v0″, 0, 1, 1, 0, m_blurConstant, 0); fragmentProgram.assemble(Context3DProgramType.FRAGMENT, m_blurAgal);

初始化一个圈数为3,强度为1,uv位置为”v0”,纹理位置在0,常量偏移为1,临时寄存器为1,常量数组,初始值在常量位置1
编译agal

最后是Demo: click me

Github地址: click me

在stage3D中实现后期处理

也许后期处理这个词很难理解,实际上就是对 GPU渲染完成的帧再进行处理。
在stage3D中,我们可以将GPU的图形渲染到纹理中,然后对这张图片进行各种处理,比如雾化,HDR, 模糊, 扭曲 等等 …

实际操作起来非常简单,代码也非常的少。 先上效果图,然后是源码,最后是Demo

postProcessing

我们要做的仅仅几步

        private function renderPostProcessing() : void
        {
            // Render the scene to the scene texture
            m_context.setRenderToTexture(m_sceneTexture, true);
            renderCube();
            m_context.setRenderToBackBuffer();

            m_context.setProgram(m_normalShader);
            m_context.setTextureAt(0, m_sceneTexture);
            m_context.clear(0.5,0.5,0.5);

            m_modelMatrix.identity();
            m_modelMatrix.appendTranslation(0,0,1.21);

            m_finalMatrix.identity();
            m_finalMatrix.append(m_modelMatrix);
            m_finalMatrix.append(m_viewMatrix);
            m_finalMatrix.append(m_projMatrix);

            m_context.setVertexBufferAt(0, m_sceneVertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_3);
            m_context.setVertexBufferAt(1,m_sceneVertexBuffer, 3, Context3DVertexBufferFormat.FLOAT_2);

            m_context.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, m_finalMatrix, true);

            m_context.drawTriangles(m_sceneIndexBuffer,0,2);
            m_context.setTextureAt(0,null);
            m_context.setVertexBufferAt(0,null);
            m_context.setVertexBufferAt(1,null);
        }

重点就3句,设置GPU输出图像到指定的纹理。
因此事先需要创建一张和视口等大的贴图
m_context.setRenderToTexture(m_sceneTexture, true);
绘制原始的场景
renderCube();
现在纹理已经被填充了,因此我们设置GPU输出到后缓冲,也就是视口
m_context.setRenderToBackBuffer();

之后我们就可以对纹理做些奇怪的事情了。
你可以按1,2,3,4,5 来看各种效果, 按0重置
Demo is Here : demo

我已经把源码上传到github
https://github.com/dreamfairy/3DDemo/blob/master/StencilShadow/src/PostProcessing.as

接下来,我打算来实现HDR效果~一定很棒!

3D世界物体拾取 续

之前的这篇文章
http://www.dreamfairy.cn/blog/index.php/2013/05/05/picking-object-in-stage3d.html

其中的公式可能有点难懂=.= , 而且有个bug, 即相机旋转后无法选择物体.

嘛~今天我们来换一种方式来实现,并且支持旋转的相机.

世界上代码,里面有注释

        /**
         * 包围球射线检测
         * param o 射线原点
         * param d 射线角度
         * param c 圆心
         * param r 半径
         */

        public static function RaySphereIntersect(o : Vector3D, d : Vector3D, c : Vector3D, r : Number) : Boolean
        {
            var intersect : Boolean;
            var s : Number;
            var l : Vector3D = c.subtract(o); //圆心到射线原点的距离
            s = d.dotProduct(l); //以射线起点开始,以射线的方向步进和包围球的长度的距离
           
            var ll : Number = l.length * l.length; //以射线的起点为圆心,以和包围球的距离为半径假设一个球形
            var rr : Number = r * r;
           
            if(s < 0 && ll > rr) return false; //s < 0 表示目标在射线之后, ll > rr 表示射线不在圆内
           
            var mm : Number = ll - s*s; //圆心垂直于射线方向的长度
            if(mm > rr) return false; //和射线构成的三角形不在圆内
           
            var q : Number = Math.sqrt(rr - mm); //圆心垂直于射线方向的点到射线碰撞的圆表面点的距离
            var t : Number; //射线起点和圆相交点的距离
            if(ll > rr)  //当射线在圆外
                t = s - q;
            else
                t = s + q;
           
            return true;   
        }
            stage.addEventListener(MouseEvent.CLICK, onClick);
        private function onClick(e:MouseEvent) : void
        {
            //将鼠标点击位置偏移视口中央
            var sceenX : Number = stage.mouseX;
            var sceenY : Number = stage.mouseY;
           
            var viewX : Number = (sceenX * 2  / stage.stageWidth) - 1;
            var viewY : Number = (-sceenY * 2 / stage.stageHeight) + 1;
           
            var ray : Ray = new Ray();

            //将射线转换到相机所在空间
            var viewMat : Matrix3D = m_camera.getViewMatrix().clone();
            ray.origin = viewMat.transformVector(new Vector3D(0,0,0));
           
            //将相机反转,相机的平移和旋转总是和世界坐标系相反的
            viewMat.invert();
            ray.direction =  viewMat.deltaTransformVector(new Vector3D(viewX,viewY,1));
            ray.direction.normalize();
           
            //取出相机的缩放值
            var scale : Vector3D = m_proj.decompose()[2];
           
            //将射线转到目标所在空间
            var cubeMesh : CubeMesh;
            for each(cubeMesh in m_cubeList)
            {

                //相交检测
                var cubePos : Vector3D = cubeMesh.transform.position;
                cubePos.x *= scale.x;
                cubePos.y *= scale.y;
                cubePos.z *= scale.z;


                if(Ray.RaySphereIntersect(ray.origin, ray.direction, cubePos, 2))
                    trace("碰撞", m_cubeList.indexOf(cubeMesh));
            }
        }

btw: 该来的总会来的,工作开始转向用cocos2d-x来制作手游了. stage3D这块还会继续研究的,只是成为副业了. o(︶︿︶)o 唉 真坑爹.

在stage3D中实现模型描边效果

效果如图~描边

实现方法就是渲染一个略大的模型,贴上一个纯色贴图,本例是白色。 然后裁减后面,渲染前面。

如果你当心渲染问题,完全没必要,首先顶点已经在GPU缓冲里了,可以复用,然后无需的面被裁减了。

Demo 地址:
Demo is here

在stage3D中行走在高低起伏的地形之上

继续地形渲染的教程~

现在我们有了凹凸不平的地形了,要如何在上面行走呢~
我们只需要一个函数就可以完成,下面我将逐行讲解这些代码,最后放出完整的函数

public function getHeight(x : Number, z : Number) : Number

创建一个函数来获取高度,传进来的值为要行走的物体的世界坐标x,y. 记住这个函数是以地形的中心点在世界中心为前提计算了,如果你的地形有偏移,请加载偏移值.

x = (m_width >> 1) + x;
z = (m_depth >> 1) - z;
//地形上的x,y. 就是以地形的中心点为前提再偏移. m_width >> 1 和 m_depth >> 1 就是将xz平面的地方 / 2 后,取到中心点.

x /= m_cellSpacing;
z /= m_cellSpacing;

var col : int = Math.floor(x);
var row : int = Math.floor(z);
//我们的地形的每个顶点的间距是 m_cellSpacing ,本demo中的值是10. /= 的方法就是求 col 和 row 的索引值,相信做过2D游戏A*寻路,或者地图区块平铺的同学知道.

现在我们取到当前的物体所在地形上的哪个顶点上, 要计算当前顶点所在平面的法线,也可以说是计算当前地形的高度和倾斜角度.
为了计算这些东西,我们需要这个平面的4个顶点,然后计算他们的差值.

var A : Number = getHeightMapEntry(row, col);
var B : Number = getHeightMapEntry(row, col + 1);
var C : Number = getHeightMapEntry(row + 1, col);
var D : Number = getHeightMapEntry(row + 1, col + 1);

这个 getHeightMapEntry 是上一教程中的函数,其内容就是取出灰度图中的数值. 内部是这样的
return m_heightMap[row * m_numVertsPerRow + col];

var dx : Number = x - col;
var dz : Number = z - row;
//我们将当前位置偏移到这个平面的左上角
           
var height : Number = 0;
if(dz < 1 - dx) //当 dz < 1-dx 时,表示我们在左上半部分的三角形上
{
    var uy : Number = B - A;
    var vy : Number = C - A;
               
    height = A + Utils.lerp(0,uy,1 - dx) + Utils.lerp(0,vy,1 - dz);
        //计算这个三角形邻边和对边的线性插值.  Utils.lerp就是计算线性插值的函数.
}else{ //当 dz > 1-dx 时,表示我们在右下半部分的三角形上
    var uy : Number = C - D;
    var vy : Number = B - D;
               
    height = D + Utils.lerp(0,uy,1 - dx) + Utils.lerp(0,vy,1 - dz);
}
           
return height;

最后线性差值的函数为

public static function lerp(a : Number, b : Number, t : Number) : Number
{
    return a - (a * t) + (b * t);
}

在stage3D中创建一个海浪的效果

最近写教程写的有点虚脱,so~ 这篇就是纯粹的吐槽. 上个月有看到老外聊天室中交流flash3D的东西,然后一个小哥做了个flare3D的帆船效果,很赞,里面包含了海浪,粒子,投影. 除了粒子,就海浪我没有实现了,于是突发奇想的自己也实现下.

实现方式其实就是先使用 Bitmapdata创建柏林噪音的灰度图, 利用shaderJob的多线程将灰度图的像素填充进byteArray. 最后用byteArray 更新海面的顶点. 海面的阴影可以利用垂直光计算,填充到贴图上.

下面这是Demo啦.

by the way~ 其实描边什么的我也都实现了,下周开始转向模型了.

在stage3D中拾取物体

思来想去,我决定先写这篇教程,地形阴影留着明晚再写.拾取物体涉及到射线,这块比较难懂,自认为比地形什么要难得多,于是决定先写拾取物体来梳理下自己的思路.

首先 屏幕上点称为 S(x,y); 视口上的点称为 P(x,y) 实际上在flash中,屏幕和视口的宽高一样,只是中心点屏幕在左上角,视口在屏幕中央.
但这里还是列举下它们的关系

Sx = Px(Width / 2) + X + Width / 2;
变形得出 Px = (2 * Sx) / Width – 1
这公式将屏幕的坐标从左上角移动到 屏幕中央(视口的起点) 后再偏移

Sy = -Py(Height / 2) + Y + Height / 2;
变形得出 Py =-(2 * Sy) / Height + 1
这公式将屏幕的坐标从左上角移动到 屏幕中央(视口的起点) 后再偏移

由于视口是投影的平面,所以我们的Pz 总是 1
所有的物体通过透视矩阵显示在屏幕上的时候,会由于相机缩放会影响到物体的大小.
这些缩放值存储在透视矩阵的 00 和 11 位置即
[00 01 02 03]
[10 11 12 13]
[20 21 22 23]
[30 31 32 33]
因此我们需要把我们求出的 Px, 和Py 除以缩放值,得出经过透视投影后物体坐标

然后我们要定义一个射线类,其中包含了射线的起点 和 射线的方向. 射线的方向是个单位向量.

package
{
    import flash.geom.Vector3D;

    public class Ray
    {
        /**起点**/
        public var origin : Vector3D;
       
        /**方向**/
        public var direction : Vector3D;
       
        public function Ray()
        {
        }
    }
}

射线的起点总是视口的中心点,即屏幕中央. 所以 origin 总是 0,0,0
用一个等式来表示方向向量 u = P – P0; P0即起点, origin.
如果P点是射线穿过投影平面上的点,那么射线的方向向量 u = 投影点P – origin = (Px,Py,1) – (0,0,0) = P; 因此我们只需要直接把投影坐标标准化转换成单位向量表示方向即可.

下面的函数来帮助我们创建一条射线,起点是屏幕中央,方向是点击屏幕后转换到投影平面的坐标标准化.

        /**
         * 计算射线
         */

        private function calcPicingRay(x : int, y : int) : Ray
        {
            var px : Number = 0;
            var py : Number = 0;
           
            var vp : Rectangle = new Rectangle(0,0,stage.stageWidth,stage.stageHeight);
           
            var rawData : Vector.<Number> = m_proj.rawData;
            px = (((2 * x) / vp.width) - 1.0) / rawData[0];
            py = (((-2 * y) / vp.height) + 1.0) / rawData[5];
           
            var ray : Ray = new Ray();
            ray.origin = new Vector3D(0,0,0);
            ray.direction = new Vector3D(px,py,1);
           
            return ray;
        }

在代码中,你可能发现 ray.direction 没有进行标准化 .normalize() .不必担心,在另一个函数里还要对射线进行加工.

现在我们已经有一条射线了,但是它只停留在投影平面上,为了进行跟3D物体的相交测试,我们还需要把射线转换到3D空间中.

做法很简单,只需要把我们的射线右乘相机矩阵即可. 即VM Vector * Matrix; 乘以的结果还是一个向量. 在转换原点时,要将原点向量的分量w = 1. 转换方向时,分量 w = 0;
转换完毕后,我们将方向的标准化 补上.

代码如下

        /**
         * 转换射线
         */

        private function transformRay(ray : Ray, t : Matrix3D) : void
        {
            //转换射线的原点, w = 1
            ray.origin.w = 1;
            ray.origin = Utils.subjectMat(ray.origin,t,ray.origin);
           
            //转换射线的方向, w = 0;
            ray.direction.w = 0;
            ray.direction = Utils.subjectMat(ray.direction,t,ray.direction);
           
            ray.direction.normalize();
        }

Utils.subjectMat 是我自己写的向量乘以矩阵的实现函数, 我不知道stage3D中是否有该API,如果有,请告知我是哪个. 这里我也提供下这个函数

        //向量左乘矩阵
        public static function subjectMat(vec : Vector3D, mat : Matrix3D, out : Vector3D) : Vector3D
        {
            var data : Vector.<Number> = mat.rawData;
           
            if(null == out) out = new Vector3D();
           
            out.x = (vec.x * data[0] + vec.y * data[4] + vec.z * data[8] + vec.w * data[12]);
            out.y = (vec.x * data[1] + vec.y * data[5] + vec.z * data[9] + vec.w * data[13]);
            out.z = (vec.x * data[2] + vec.y * data[6] + vec.z * data[10] + vec.w * data[14]);
            out.w = (vec.x * data[3] + vec.y * data[7] + vec.z * data[11] + vec.w * data[15]);
           
            return out;
        }

现在我们有了3D空间中的射线了,可以做相交测试了.
我们可以遍历视口中的所有三角形,判断哪些三角形被射线击中了,虽然很精确,但是性能并不高.
因此在本例中我们用包围球来碰撞,实际上你也可以自己试试AABB碰撞.

给出包围球的圆心C 和 半径R. 使用下面的恒等式就能测试点 P 是否在包围球上.

||P-C|| – R = 0;

向量 P 到 圆心 C 的长度表示为 ||P-C||. 如果等于半径C 表示P在包围球上.
现在把我们的射线公式带入这个相交的公式中 P = P0 +tU;
|| P0 + tU – C || – R = 0;

由此我们我们可以退出一元二次方程
At^2 + Bt + C = 0; 这个公式很熟悉吧~ 貌似初中就学过了.
t0 = (-B + Sqrt(B * B – 4AC) / 2;
t1 = (-B – Sqrt(B * B – 4Ac) / 2;
因为U是表示方向的单位向量,因此A = 1; 带入即可求出 t0,t1
当t0 或者 t1 >= 1 时,表示切线,相交 等情况. <0 表示完全没有相交,或者在物体的前方. 最后我们通过下面的函数来做碰撞检测 由于仅仅是为了做碰撞检测,这里没有具体的实现包围球,但是本例的Cube顶点的间距是 从 -1 ~ 1 而圆心是在 Cube 的中央,因此 Cube的直径是 2. 那么半径 cube.radius 我们就知道是 1 了.

        /**
         * 碰撞检测
         */

        private function raySphereIntTest(ray : Ray, cube : CubeMesh) : Boolean
        {
            var v : Vector3D = ray.origin.subtract(cube.position);
            var b : Number = 2 * ray.direction.dotProduct(v);
            var c : Number = v.dotProduct(v) - (cube.radius * cube.radius);
           
            var discriminant : Number = (b * b) - (4 * c);
           
            if(discriminant < 0)
                return false;
           
            discriminant = Math.sqrt(discriminant);
           
            var s0 : Number = (-b + discriminant) / 2;
            var s1 : Number = (-b - discriminant) / 2;
           
            if(s0 >= 0 || s1 >= 0)
                return true;
           
            return false;
        }

最后的最后我们将这些代码串起来,就是下面的Demo了.
首先是 stage.addEventListener(MouseEvent.CLICK, onClick);
然后是

        private function onClick(e:MouseEvent) : void
        {
            var ray : Ray = calcPicingRay(e.stageX,e.stageY);
            var viewInverse : Matrix3D = m_viewMatrix.clone();
            viewInverse.invert();
            transformRay(ray,viewInverse);
           
            if(raySphereIntTest(ray,m_cubeList[0])){
                tip.text = "碰撞";
            }  
            else{
                tip.text = "W,S,A,D 控制飞船移动n方向键控制相机移动";
            }  
        }

你想问 m_cubeList[0] 是哪个? 我实际上我创建的30个Cube中的第一个,而且为了让我们知道哪个是它,我让它一直旋转.

每次刷新 Cube的位置都会变化.
Demo is Here

在stage3D中动态创建贴图并绘制地形阴影

昨天生成地形后贴图
terrain

这张图,用肉眼很难分辨高海拔和低海拔区域,毕竟没有参考物.类似雪盲症的效果.
那么我们可以动态创建贴图,在海拔较高的地方填充白色,感觉像雪山.海拔较低的地方填充亮黄色,像沙地,其他部分就填充绿色.

还记得之前我们灰度图读取出来的值都存在 m_heightMap 中吗? 这个数组存储的长度和我们贴图的像素数量一样的.这样我们就可以遍历像素的同时,取出当前像素对应的高度值,然后根据高度值填充我们想要的颜色.

代码如下

        public function genTexture(light : Vector3D) : void
        {
            var texWidth : int = m_numVertsPerRow;
            var texHeight : int = m_numVertsPerCol;
           
            var emptyBmd : BitmapData = new BitmapData(texWidth, texHeight,false);
            emptyBmd.lock();
           
            for(var i : int = 0; i < texHeight; i ++)
            {
                for(var j : int = 0; j < texWidth; j++)
                {
                    var c : uint;
                    var height : Number = getHeightMapEntry(i,j) / m_heightScale;
                   
                    if(height < 42.5) c = Utils.BEACH_SAND;
                    else if(height < 85) c = Utils.LIGHT_YELLOW_GREEN;
                    else if(height < 127.5) c = Utils.PUREGREEN;
                    else if(height < 170) c = Utils.DARK_YELLOW_GREEN;
                    else if(height < 212.5) c = Utils.DARKBROWN;
                    else c = Utils.WHITE;
                   
                    emptyBmd.setPixel32(j,i,c);
                }
            }
           
            emptyBmd.unlock();
            setTexture(emptyBmd);
        }

        public function getHeightMapEntry(row : int, col : int) : int
        {
            return m_heightMap[row * m_numVertsPerRow + col];
        }

        public function setTexture(data : BitmapData) : void
        {
            m_texture = Utils.getTextureByBmd(data, m_context3D);
        }

当创建好这张动态贴图并贴上之后,就是下图的样子了.
followHeight

继续阅读“在stage3D中动态创建贴图并绘制地形阴影”

在stage3D中使用高度图来绘制地形

本文已抄送到天地会 : http://bbs.9ria.com/thread-192787-1-1.html

这是效果图
terrain

首先先准备一张高度图
heightMap
你可以用Photoshop制作,在创建图片的时候就选择灰度图. 当你制作完毕一张灰度图的时候,保存的格式最好是8Bit的RAW文件,这样每个像素都被保存在 unsignedbyte 中,且没有各种烦人的文件头信息.有且只有像素信息.

为了方便测试,在AS中我们将这个RAW文件嵌入到项目中,用下面代码

        [Embed(source="../../source/coastMountain64.raw",mimeType = "application/octet-stream")]
        private var terrainData : Class;

当文件嵌入后,我们就开始着手解析了.解析的方法很简单,按顺序读取字节流,然后存进一个 Vector. 数组中.

        private function parseHeightMaps(data : ByteArray) : Boolean
        {
            if(null == data || 0 == data.bytesAvailable) return false;

            var len : uint = data.bytesAvailable;
            for(var i : int = 0; i < len; i++){
                m_heightMap.push(data.readUnsignedByte());
            }
           
            return true;
        }

现在我们有了地形的数据了,为了让其显示出来,我们开始创建顶点和索引
顶点的格式是 [x,y,z,u,v] 5个数据
如果每个顶点之间是紧密排列的话,其中u,v的递增量可以通过下面的方式计算出.

            var uCoordIncrementSize : Number = 1.0 / 每列顶点数;
            var vCoordIncrementSize : Number = 1.0 / 每行顶点数;

由于高度图是 64 * 64 的大小,我们做 64 * 64的循环来取顶点
startZ = 0, endZ = 63;
startX = 0, endX = 63;
如果顶点间是紧密排列的. 那么 m_cellSpacing 为1.0

            for(z = startZ; z >= endZ; z -= m_cellSpacing)
            {
                j = 0;
                for(x = startX; x <= endX; x += m_cellSpacing)
                {
                    //计算当前顶点缓冲的索引,避免死循环
                    var index : int = i * m_numVertsPerRow + j;
                    m_rawVertex.push(x,m_heightMap[index],z,j * uCoordIncrementSize,i * vCoordIncrementSize);
                    j++;
                }
                i++;
            }

解析完顶点后,就要开始计算索引了.
indexbuffer

通过这张图,可以理解到,2个三角形和顶点数组中索引对应的上下两行的关系.
当前顶点的 index, index + 1 和下一行的 index 构成第一个三角形, 另一半三角形也同样. 他们共用 C 顶点.
转换成代码就是如下的形式

        private function computeIndices() : void
        {
            var baseIndex : int = 0;
           
            for(var i : int = 0; i < m_numCellsperCol; i++)
            {
                for(var j : int = 0; j < m_numCellsPerRow; j++)
                {
                    m_rawIndex[baseIndex]       = i * m_numVertsPerRow + j;
                    m_rawIndex[baseIndex + 1]   = i * m_numVertsPerRow + j + 1;
                    m_rawIndex[baseIndex + 2]   = (i + 1) * m_numVertsPerRow + j;
                   
                    m_rawIndex[baseIndex + 3]   = (i + 1) * m_numVertsPerRow + j;
                    m_rawIndex[baseIndex + 4]   = i * m_numVertsPerRow + j + 1;
                    m_rawIndex[baseIndex + 5]   = (i + 1) * m_numVertsPerRow + j + 1;
                   
                    baseIndex += 6;
                }
            }
            trace("索引解析完毕");
        }

现在我们有了顶点,也有了索引.就可以将其显示出来了.
贴图什么随便设定就好了.

demo is here

你可能会觉得地形很难看出高低的感觉,这是因为没有地形没有阴影,当然这是下一篇进阶的功能了.