在Stage3D中创建水体反射效果

对于水来说,应该具备下列的特征

  • 全反射,所有的物体都应该能被反射到,除了反射表面自身
  • 基于角度的反射
  • 深度穿透,近距离的水更加透明,远距离的水更加浑浊
  • 加入水纹或者水面法线

对于本文来说只实现前面2点

1.1视差
如果你之前渲染引擎使用单个pass,那么从现在开始,你需要至少2个passes(实际上至少是 N+1, N为可见的反射物体数量)

原因是,我们无法复用主场景的纹理用来反射。首先是因为视锥体可能非常大(比如,观察水表面从一个非常高的角度,我们只能在主场景中看到地面和水,还有被反射的大部分的天空)。 第二是因为视差,很不幸的是反射并不是主场景的完美复制,只是从不同的视觉位置的场景反射。下面的图表现了这个观点。

reflections

这意味着你需要渲染到纹理,我们将要渲染反射到纹理,然后使用这个纹理来渲染我们主场景的水面

因此,要得到反射纹理,我们首先需要通过反射相机来渲染我们的场景 如之前图中的 P` 位置。 首先我们需要找到反射相机的位置,或者更精确的说,相机矩阵的反射(因为我们还需要相机的角度), 我们能通过下面的算法得到反射矩阵

M’camera = Mreflection * Mcamera

Mreflection 是镜面反射矩阵,它能直接计算出基于平面一个点的反射

| 1-2Nx2 -2NxNy -2NxNz -2NxD |
Mreflection = | -2NxNy 1-2Ny2 -2NyNz -2NyD |
| -2NxNz -2NyNz 1-2Nz2 -2NzD |
| 0 0 0 1 |

(Nx,Ny,Nz,D) 是平面的方程 (xNx + yNy + zNz + D = 0) 的系数, 注意: (Nx, Ny, Nz) 也是这个平面的法线向量

Mcamera 是相机的矩阵变换, 在场景中也起着法线的功能, 要获取 ModelView 矩阵,你还需要将相机矩阵翻转

AS的计算函数如下
[cc lang=”actionscript3″]
//计算平面投影矩阵,n为平面法向量,p为平面上一点
public static function getReflectionMatrix(n:Vector3D,p:Vector3D):Vector.{
n.normalize();
var d:Number=-n.dotProduct(p);

var raw:Vector.=Vector.([-2*n.x*n.x+1,-2*n.y*n.x,-2*n.z*n.x,0,
-2*n.x*n.y,-2*n.y*n.y+1,-2*n.z*n.y,0,
-2*n.x*n.z,-2*n.y*n.z,-2*n.z*n.z+1,0,
-2*n.x*d,-2*n.y*d,-2*n.z*d,1]);
return raw;
}
[/cc]

1.2 镜面几何体
事实上,在之前的图形中,我们作弊了,我们旋转了镜面纹理180°使得它与原始图更相似,使得视差的效果能被看到。实际上,镜面图应该看起来像这样。

注意:球型物体的排序顺序在反射贴图中被翻转了,因为三角形顺序在主场景中是逆时针,但在反射中是顺时针

这对于你来说并不是问题,如果你所有的材质都是双面渲染的(比如,你不切除后表面) 或者如果你能设置以类似的方reflections-2式渲染管线。
这样你就能改变剔除方向。在我的例子中,我尽量设置双面都不剔除, 所以所有的东西都会出现在反射纹理中,反则一些几何体不会被渲染。

我们将要开发一个功能,使得相机总是(至少在大多数应用中)与视觉方向成直角并且处于中央,因此我们近需要翻转相机的Y轴方向然后改变渲染顺序就行(在翻转反射纹理后,看起来像 第一张图片中的 (3) 部分).
我们能再通过一个反射矩阵来实现

M”camera = Mreflection * Mcamera * Mflip

1.3 水下翻转
看看下面这张图

reflections-3

我们加入一个水下物体Q到我们的场景中,现在它应该不会出现在反射中,因为因为它不会阻挡射线 PB’B 和 PA’A. 但是我们并没有做射线追踪。 我们通过移动相机到镜像位置 P’ 然后像一般纹理一样渲染反射。 但是如你所见,对象Q阻挡了射线 P’A’A 因此物体Q会出现在我们的反射中

因此我们需要确定,任何物体在反射平面(水面)之下的都不能出现。我们能通过下面3种方式来实现。

1.在GPU中使用额外的裁切面。这可能非常快,也可能非常慢,取决与你的图形卡。
2.在反射渲染中使用斜投影矩阵,你可以在 http://www.terathon.com/code/oblique.html 了解它的相关知识。这是一个非常cool的技术,但是它在远平面中的相机中表现的并不好。
3.手动修剪 Pixel Shaders. 这会浪费一些GPU的循环,但是却非常简单。

我选择使用方法3,因为斜投影矩阵并没有看起来那么好,当相机的角度很大(远平面都变成一个非常奇特的效果)。 剪裁功能非常容易加入到下面的代码中,在所有的 Pixel Shaders 开始之前(更确切的说是用在所有的反射物体上)

[cc lang=”actionscript3″]
//float clipPos = dot (interpolatedVertexEye, clip_plane.xyz) + clip_plane.w;
dp3 result.x eyePos.xyz plane.xyz
add result.x result.x plane.www
//if (clipPos < 0.0) {
// discard;
//}
slt ifReg.x result.x fc0.x //fc0.x == 0
[/cc]

你必须计算顶点Shader(将顶点坐标转换到视野坐标下 VertexEye = Mmodelview * Vertex)中计算平面到视角的差值. 如果你不要裁剪,只要设置 clip_plane 法线(xyz) 为0. 这样所有的像素都会被渲染

1.4 把一切都合并起来
在开始主渲染pass之前(提前或者推迟)做下列这些事

1.创建一个需要被反射的渲染列表(参数为它们的反射面)之后 for each 反射面:
2.计算反射相机矩阵 M”camera = Mreflection * Mcamera * Mflip
3.设定相机矩阵(你可以通过使用翻转投影矩阵来优化,但是在这里不适用)
4.设置裁剪面到反射平面
5.渲染整个场景
6.保存反射纹理应用到反射物体上

如果你使用HDR, 你就不要用使用色调投影到反射纹理上,除非你想做一些非常奇特的效果。

2.渲染反射物体
这一步非常的简单,确保你掌握了所有必须的参数。你还需要决定哪一个 render stage 来做这些事。我使用Transparent.
这样水面就是基于一个场景的透明表面。但是你可以增加一些其他的pass在之前或者之后。

你需要掌握这些
反射相机矩阵 M”Camera
投影矩阵你需要渲染反射 Mprojectionreflection(通常是和你的主相机是使用的同一个矩阵)
反射纹理

2.1 顶点Shader
参数
vertex3D vertex;
Matrix3D o2v_projection;
vertex3D interpolatedVertexObject;

m44 vt0 02v_projection vertex3D(vertex.x,vertex.y,0.1)
interpolatedVertexObject = vt0

我们在这里增加了一个约束,水面在物体本地坐标系中是属于 XY 平面。 它不是确实不是必要的如果你有适当的反射平面,但是我发现这样做起来更容易。仅仅使用XY平面作为反射平面然后适当摆放你的物体(水体)

事实上,有另一个很酷的诀窍,我们可以使用水体的底部作为我们的水体。它将会在Shader中被扁平化,我们可以使用 Z data 来检测水中点的深度。 这部分以后再说。

o2v_projection 仅仅是 Projection * modelView 矩阵的名称。 我喜欢这样给矩阵命名,这样跟助于记忆, 使用它们所描述的坐标系统。 比如 Object To View * Projection

interolatedVertexObject 仅仅是顶点坐标在本地空间的坐标系统,我们将需要在反射纹理中看到它。

2.2 Fragment Shader
参数
matrix3D o2v_projection_reflection
sampler2D refletion_sampler
vertex3D interpolatedVertexObject

vClipReflection = o2v_projection_refletion * vectex(interpolatedVertexObject.xy,0.0,1.0);
//设备反射
vDeviceReflection = vClipReflection.st / vClipReflection.q
//纹理反射
vTextureReflection = vec2(0.5,0.5) + 0.5 * vDeveiceReflection;
//反射纹理颜色
reflectionTextureColor = texture2D(reflection_sampler, vTextureReflection);

//反射纹理的Alpha 可能 > 1
reflectionTextureColor = 1.0;

mov oc reflectionTextureColor

o2vProjection_reflection 是 Projection * ModelView 矩阵, 用来在反射期间进行渲染

Mprojectionreflection * (M”camera)-1 * Mobject

如名字表现的,这是将物体空间坐标系统转换到切面坐标系统的反射相机
在fragment shader 中,我们仅仅重复了整个完整的变换管线在反射渲染中。 渲染然后使用2D坐标系采样。要做到这样,我们首先需要初始化和为转换的顶点物体坐标,因此他们被差值通过vertex shader(interpolatedVertexObject)

设置反射alpha 为 1 因为我使用了 HDR 缓冲,因此它的的 alpha 最终会变成一个非常奇怪的值

下面我们用stage3D来实现它

首先准备天空盒,水面,和水面上一个物体 渲染

normal

 

之后通过反射矩阵反射相机,对屏幕进行RTT

反射后的结果是这样reflection

接下来就是作弊的地方了,我们需要对RTT进行翻转,在 Fragment Shader 中翻转UV太麻烦,只需要对反射相机矩阵 appendScale(1,-1,1) 即可垂直翻转了

之后将RTT的纹理赋予水体表面

但是对于水体来说,我们仅仅需要水体反射部分的纹理像素矩形,其他部分都不需要。

因此需要将水体的顶点转换到设备空间(-1~1)后,再转换到纹理空间(0~1)采样即可

算法在前文的glsl代码中有

这里贴出agal

[cc lang=’actionscript3″]

var fragment : AGALMiniAssembler = new AGALMiniAssembler();
fragment.assemble(Context3DProgramType.FRAGMENT,
[
“div ft1.xy v3.xy v3.w”,
“mov ft2 fc0”,
“mul ft1.xy ft1.xy ft2.xy”,
“add ft1.xy ft1.xy ft2.zw”,
“tex ft0, ft1.xy, fs0<2d,wrap,linear>”,
“mov oc, ft0”,
].join(“n”));

[/cc]

最后大功告成

result

 

最后我闲的无聊,混合一个蓝色给水面…

发表评论

电子邮件地址不会被公开。 必填项已用*标注