物体投影在另一个物体身上,而另一个物体可能是平的,但大多数都是不平的多边形物体,这里考虑的是后者,这样可以适用于大多数场合的投影。
预览效果:
原理:
这里使用的是shadow mapping方式,其原理如下:
1、将场景的深度值预先渲染到 以光源位置为原点、光线发射方向为观察方向的投影坐标系中,形成深度纹理。2、再次渲染场景的过程中,将每个片断(像素)变换到前述眼坐标系中,并缩放到[0,1]的范围内以便查询纹理。
3、以较暗的光照绘制场景
4、以当前片断在眼坐标中的S、T坐标查询深度纹理获得深度值,将此深度值与当前片断的R坐标进行比较,若R坐标大于深度值,则当前片断在阴影中;否则当前片断受光照。
转换为stage3D后的流程
1、获得深度图:将要投影的模型以光点的视角绘制成图,.xyz取值[0,1],储存深度
2、绘制被投影物体:
-- 顶点仍然要以光点的视角转换(P),以便计算(但输出仍是实际相机视角输出)
-- 取得光点视角的点对应的深度图上的深度,通过将光视角上的点P转为设备坐标(范围[-1,1])P2,再转为深度图的坐标P3,
这样就相当于计算出了相对于深度图的正确UV坐标,这个时候可以用该UV去取得深度图的纹理了
-- 取得深度图中的该点对应P的深度值与该点P对比,大了就可以涂黑了
鼠绘图说明:
1、这个是以灯光视角查看物体和场地的效果预览,黄色是灯光,黑色是投影物体,蓝色是被投影的场地。
2、以相机视角查看效果预览,这里可以看得出理论上应该是投影成这个样子的(只是随手一画,并不严谨,参考即可)
3、我们以灯光视角来绘制投影物体(黑),但只储存深度信息(z轴),所以越远越白,越近越黑,储存到纹理上
4、绘制被投影物体,要拿刚才那个深度图纹理对比,取得的点必须正确,如果取得红色点的话就是错的了,就会像下图那样显示在物体中间了,其实应该只显示一半的
关键代码--获得深度图:
"注意,由于我们这里绘制的图是越离光近越黑越接近0,越远越白越接近1,所以默认让画布是白的表示其他地图最远,否则计算结果不正确"
context3d.clear(1,1,1);
// vc0 MLP
finalMatrix.identity(); // 归零
finalMatrix.append(modelMatrix); // M 投影物体的模型矩阵
var lightMatrix:Matrix3D = new Matrix3D(); // 计算光点矩阵L(以光点为视角)
lightMatrix.appendTranslation(lightPos.x,lightPos.y,lightPos.z);
finalMatrix.append(lightMatrix.clone());
finalMatrix.append(projectionMatrix); // P矩阵
// fc0 用到两个常量 fc0.x=zFar 和 fc0.w 使生成的纹理.w=1
context3d.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT,0,Vector.<Number>([zFar,0,0,1]));
// vertex shader
"m44 vt0 va0 vc0 \n" // 最终输出点vt0 = 原始顶点*MLP
"mov v1 vt0\n" + // 将它传输给片段着色器使用
"mov op vt0" // 将它输出
// fragment shader
"mov ft0.xyz v1.zzz\n" + // 让图片着色统一是z,取值范围 [0,zFar]
"mov ft0.w fc0.w\n" + // 图片色彩w = 1 即透明度
"div ft0.xyz ft0.xyz fc0.x\n" + // ft0.xyz = ft0.xyz / zFar 让取值变为范围[0,1]
"mov oc,ft0" // 输出,由于颜色值根据深度归为0~1,所以越远越接近1越白,越近越黑
// 获得图(由于这里不需要再在CPU上处理,直接GPU上处理即可,所以使用setRenderToTexture)
context3d.setRenderToTexture(texShadow,false,0,0); // 设置即将要渲染到的位图
context3d.clear(0,0,0); // 填色黑色
lightViewEntity(); // 渲染物体(绘制深度图)
context3d.setRenderToBackBuffer(); // 设置渲染到后台缓冲区(配合setRenderToTexture使用,用后恢复正常)
// 如果必须要到CPU上处理,还可以这样获得位图图源到bdShadow:BitmapData,
context3d.clear(0,0,0);
lightViewEntity();
context3d.drawToBitmapData(bdShadow);
texShadow.uploadFromBitmapData(bdShadow);<strong>
</strong>
关键代码--渲染被投影物体:
// 灯光坐标点
private var lightPos:Vector3D = new Vector3D(0,0,-15,0);
// vc0 MVP 相机视角最终矩阵
finalMatrix.identity();
finalMatrix.append(floorMatrix);
finalMatrix.append(viewMatrix);
finalMatrix.append(projectionMatrix);
context3d.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX,0,finalMatrix,true);
// vc4 MLP 灯光视角最终矩阵
finalMatrix.identity();
finalMatrix.append(floorMatrix);
var lightMatrix:Matrix3D = new Matrix3D();
lightMatrix.appendTranslation(lightPos.x,lightPos.y,lightPos.z);
finalMatrix.append(lightMatrix.clone());
finalMatrix.append(projectionMatrix);
context3d.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX,4,finalMatrix,true);
// -- 设置状态机当前的VB的坐标 到 va0 ,因为只有XYZ,所以偏移是0,长度是3
context3d.setVertexBufferAt(0,modelVbFloor,0,Context3DVertexBufferFormat.FLOAT_3);
// -- 设置状态机当前的VB的UV纹理坐标 到 va2
context3d.setVertexBufferAt(2,modelVbFloor,6,Context3DVertexBufferFormat.FLOAT_2);
// fc2 = x=zFar y=0.5(用于辅助计算)
context3d.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT,2,Vector.<Number>([zFar,0.5,1,1]));
// vertex shader
"m44 vt0 va0 vc0 \n" + // vt0 = 原始顶点*MVP 相机视角
"m44 vt1 va0 vc4\n" + // vt1 = 原始顶点*MLP 光视角
"mov v1 vt1\n"+ // 灯光视角后的顶点传输到片段着色器
"mov v1 va2\n"+ // 被投影物体的UV
"mov op vt0\n" // 输出顶点(这个仍然是相机视角的)
// fragment shader
"tex ft1 v1 fs1<2d,linear,repeat,nomip>\n"+ // 被投影物的纹理采样
"div ft2.xy v0.xy v0.zz\n"+ // 转为设备空间,x和y的取值范围是[-1,1] 就是三维点转屏幕上的二维点
"mul ft2.xy ft2.xy fc2.y\n"+ // *0.5 为适应贴图坐标系([0,1]),我们将[-1,1]转为[0,1],转换方法是new=old*0.5+0.5,然后Y取反
"add ft2.xy ft2.xy fc2.y\n"+ // +0.5
"neg ft2.y ft2.y\n"+ // Y取反,因为屏幕坐标系Y是+往上,贴图坐标中Y是-往上,最终计算出来的可以说是UV坐标
"tex ft3 ft2.xy fs0<2d,linear,repeat,nomip>\n"+ // 根据计算出来的UV坐标和深度图纹理取得纹理
"mul ft5.z ft3.z fc2.x\n"+ // z扩大zFar,让原来的0-1变为0-zFar
"slt ft5.w ft5.z v0.z\n"+ // 如果深度图中的深度 小于 (顶点xMLP)的深度的话就是1,否则就是0 也就是说影子时1,非影子0
"mul ft5.w ft5.w fc2.y\n" + // 作出效果,让 影子1,非影子0 转为影子0.5,非影子1,这样直接与原始颜色相乘让影子部分的颜色等于原来的一半
"neg ft5.w ft5.w\n" +
"add ft5.w ft5.w fc2.y\n" +
"add ft5.w ft5.w fc2.y\n" +
"mul ft1 ft1 ft5.w\n"+
"mov oc ft1\n" // oc ft1<strong>
</strong>
注意点:
1、绘制深度图时由于我们这里绘制的图是越离光近越黑越接近0,越远越白越接近1,所以默认让画布是白的表示其他地图最远,否则会认为黑色部分都是遮挡部分
2、在绘制被投影物体的时候,如何转换坐标来对比,这里还是要临时转到灯光视角,然后计算到该视角中顶点对应的深度图纹理坐标,原理:
--- pMLP = 原始顶点 * 灯光最终矩阵 = p * MLP (这里的L就是V,只不过和灯光一个位置和角度了)
--- SB(pMLP) = pMLP.xy / pMLP.zz (就是x和y分别除以z,这样就转为屏幕坐标系了,就是中心点为0,0,左边框x=-1,右边框x=1,上边框y=1,下边看y=-1的这个坐标系),也就是说x和y的取值范围都是 [-1,1]
--- 因为贴图纹理坐标是[0,1],所以这里要把屏幕中的坐标对应那张深度图(屏幕大小的截图)的坐标,要转换坐标,并且Y是相反的 UV(pMVP) *0.5 + 0.5 = [0,1] ,同时纹理y=1-屏幕坐标系的y
范例工程下载:
链接:http://pan.baidu.com/s/1o84C3rk 密码:3cvo
全代码贴出(这里还在CPU上计算了顶点最终的对应的屏幕像素坐标,方便理解):
package
{
import com.adobe.utils.AGALMiniAssembler;
import com.adobe.utils.PerspectiveMatrix3D;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.display.Stage3D;
import flash.display3D.Context3D;
import flash.display3D.Context3DCompareMode;
import flash.display3D.Context3DProgramType;
import flash.display3D.Context3DRenderMode;
import flash.display3D.Context3DTextureFormat;
import flash.displa