纹理投影效果

获取示例代码,本文代码在分支chapter21中。


本文作为OpenGL ES高级篇的开篇,将为大家介绍如何把一张纹理贴图投影到复杂的几何体上。下面是例子运行的结果。 下面是未开启纹理投影的效果图。 这张是用来投影的纹理图。

涉及到的重要知识点

  1. 基本几何体的渲染
  2. 法线贴图
  3. 纹理坐标
  4. 投影矩阵

前两个知识点是为了渲染场景,例子渲染的场景很简单,使用3个来自obj文件的正方体,加上4张贴图,只要看过前面的文章,可以很容易的理解代码。纹理坐标和投影矩阵是实现这个效果的关键。

基本原理

这个效果和真实生活中的投影仪很像。假设我们把投影仪当做一个摄像机,并且设置好投影矩阵,如下图所示。那么投影仪就具备了MVP中的VP两个矩阵了。

3D物体的顶点经过投影仪的VP和自身的M变换后就会变成投影仪空间的坐标。因为所有经过投影变换的点如果在可视范围内,它的x,y都会落在-1和1之间,所以我们可以将投影仪空间的坐标当做UV来使用,UV的范围是0~1,-1~1的范围只要加1再乘以0.5就可以很方便的变换成0~1的范围。这样我们就可以根据顶点在投影仪空间的坐标来获取UV了。

Fragment Shader

本文使用的Fragment Shader在fragment.glsl文件中。首先我们添加投影的纹理贴图和投影矩阵。

// projectors
uniform mat4 projectorMatrix;
uniform sampler2D projectorMap;
uniform bool useProjector;

useProjector主要用作开关投影效果。projectorMatrix由投影仪的投影矩阵和观察矩阵相乘而来。

接着根据投影仪空间的坐标计算UV。

vec4 positionInProjectorSpace = projectorMatrix * modelMatrix * vec4(fragPosition, 1.0);
positionInProjectorSpace /= positionInProjectorSpace.w;
vec2 projectorUV = (positionInProjectorSpace.xy + 1.0) * 0.5;

positionInProjectorSpace /= positionInProjectorSpace.w;之所以要除以w,是因为如果你使用的是透视投影矩阵,那么有可能w不为1,这时候需要xyzw都除以w保证xy范围的正确性。

最后我们判断UV的坐标来决定是否接受到了投影。UV超出0~1范围的说明不在投影仪的可视范围内,所以不需要进行投影处理。这里我使用4:6的比例简单的混合了两种颜色。你也可以把它当做光照颜色来处理,使用它计算diffuse和specular的颜色。

if (projectorUV.x >= 0.0 && projectorUV.x <=1.0 && projectorUV.y >= 0.0 && projectorUV.y <=1.0) {
    projectorColor = texture2D(projectorMap, projectorUV);
    gl_FragColor = vec4(finalColor * 0.4 + projectorColor.rgb * 0.6, 1.0);
} else {
    gl_FragColor = vec4(finalColor, 1.0);
}

上面便是Fragment Shader中和投影效果相关的代码。

生成并刷新投影仪的矩阵

我们回到OC代码来生成投影仪需要的VP矩阵。每次渲染时我都会刷新projectorMatrix,让它围绕y轴旋转。只要让camera的up向量围绕y轴旋转就可以做到这一点。随着projectorMatrix的旋转,投影的纹理也会随之旋转。

@property (assign, nonatomic) GLKMatrix4 projectorMatrix;
...

// update projector matrix
GLKMatrix4 projectorProjectionMatrix = GLKMatrix4MakeOrtho(-2, 2, -2, 2, -100, 100);
GLKMatrix4 projectorCameraMatrix = GLKMatrix4MakeLookAt(0, 4, 0, 0, 0, 0, cos(self.elapsedTime), 0, sin(self.elapsedTime));
self.projectorMatrix = GLKMatrix4Multiply(projectorProjectionMatrix, projectorCameraMatrix);

然后把projectorMatrix赋值给uniform就可以了。

[obj.context setUniformMatrix4fv:@"projectorMatrix" value: self.projectorMatrix];

本例中我使用的是正交投影,如果你想尝试透视投影,可以自行修改测试。

投影纹理

针对投影纹理需要处理的事情很简单,初始化&设置uniform。

UIImage *projectorImage = [UIImage imageNamed:@"squarepants.jpg"];
self.projectorMap = [GLKTextureLoader textureWithCGImage:projectorImage.CGImage options:nil error:nil];
...
[obj.context bindTexture:self.projectorMap to:GL_TEXTURE2 uniformName:@"projectorMap"];

纹理投影效果有什么用?

比如枪击游戏里的弹痕就可以使用这种技术来实现,将弹痕贴图投影到任何你打中的地方。而且投影贴图完全可以是动态的,加上法线贴图等等。你可以发挥自己的想象,做出一些更有意思的效果。

Updated: