獲取示例代碼
前言
本文將介紹3D游戲中一種常見的技術Billboards,又可以稱為公告板技術。形象的來說,就好像一個人舉著牌子,無論你從哪個方向看向牌子,那個人都會把牌子朝著你的方向旋轉,你永遠只能看到牌子的正面。在3D游戲中,可以用來制作npc頭上的名字或是任務標記,用來制作較遠處的樹木,路燈也是可以的。本文的例子制作的就是基于公告板的樹木。下面是運行效果。不過樹木屬于比較特殊的公告板,它只能繞著垂直軸旋轉,后面會詳細介紹這一點。
原理
公告板技術主要利用一個正方形網格,根據當前的攝像機變換信息,調整至面向攝像機的位置。這里我們主要利用攝像機向上和向右的兩個向量來調整正方形網格的點。
首先我們找到網格中心的位置,設其為
CenterPosition
,類型為三維向量。設我們想要的網格大小為Size
,類型為二維向量。攝像機向上的向量為VecUp
,向右的為VecRight
。我們使用的正方形網格初始頂點數據如下。
- (GLfloat *)planeData {
static GLfloat planeData[] = {
-0.5, 0.5f, 0.0, 0, 0, 1, 0, 0,
-0.5f, -0.5f, 0.0, 0, 0, 1, 0, 1,
0.5f, -0.5f, 0.0, 0, 0, 1, 1, 1,
0.5, -0.5f, 0.0, 0, 0, 1, 1, 1,
0.5f, 0.5f, 0.0, 0, 0, 1, 1, 0,
-0.5f, 0.5f, 0.0, 0, 0, 1, 0, 0,
};
return planeData;
}
對于每個頂點,我們可以使用下面這個公式求解。
NewPosition = CenterPosition + VecUp * OldPosition.x * Size.x + VecRight * OldPosition.y * Size.y
。這個公式的含義就是在攝像機的xy坐標系內生成一個長寬各與x,y軸平行的四邊形。四邊形長寬就是Size
。
Shader
為了支持Billboards,需要編寫新的Vertex Shader。主要增加的下面的代碼。完整代碼在vtx_billboard.glsl
中。
vec3 cameraRightInWorldspace = vec3(cameraMatrix[0][0], cameraMatrix[1][0], cameraMatrix[2][0]);
vec3 cameraUpInWorldspace = vec3(0.0, 1.0, 0.0);
if (lockToYAxis == false) {
cameraUpInWorldspace = vec3(cameraMatrix[0][1], cameraMatrix[1][1], cameraMatrix[2][1]);
}
vec3 vertexPositionInWorldspace = billboardCenterPosition + cameraRightInWorldspace * position.x * billboardSize.x +
cameraUpInWorldspace * position.y * billboardSize.y;
fragPosition = vertexPositionInWorldspace;
gl_Position = vp * vec4(vertexPositionInWorldspace, 1.0);
我們可以通過使用cameraMatrix
的逆矩陣和攝像機初始的up,right向量相乘獲得當前的up和right。不過有更簡單的方法直接從cameraMatrix
中獲得。
vec3 cameraRightInWorldspace = vec3(cameraMatrix[0][0], cameraMatrix[1][0], cameraMatrix[2][0]);
...
cameraUpInWorldspace = vec3(cameraMatrix[0][1], cameraMatrix[1][1], cameraMatrix[2][1]);
如果我們想要網格只圍繞Y軸旋轉的話,把cameraUpInWorldspace
設置為vec3(0.0, 1.0, 0.0);
即可。
最后就是使用上面提到的公式,為每個頂點重新計算位置。
vec3 vertexPositionInWorldspace = billboardCenterPosition + cameraRightInWorldspace * position.x * billboardSize.x +
cameraUpInWorldspace * position.y * billboardSize.y;
注意這里我并沒有使用modelMatrix,因為變換信息都是通過billboardCenterPosition
等uniform變量傳遞或者計算出來的。
我也為Billboard重新編寫了Fragment Shader,不過很簡單,只是在fragment.glsl的基礎上去掉了光照模型而已。最主要的就是增加了AlphaTest,完全透明的像素被直接忽略掉。
void main(void) {
vec4 diffuseColor = texture2D(diffuseMap, fragUV);
if (diffuseColor.a == 0.0) {
discard;
}
vec3 finalColor = colorWithFog(diffuseColor.rgb);
gl_FragColor = vec4(finalColor, 1.0);
}
OC代碼
OC代碼基本復用平面幾何體Plane的代碼,在繪制的時候將Billboards特有的幾個參數傳遞進去。完整代碼在Billboard.m
中。
[glContext setUniform2fv:@"billboardSize" value:self.billboardSize];
[glContext setUniform3fv:@"billboardCenterPosition" value:self.billboardCenterPosition];
[glContext setUniform1i:@"lockToYAxis" value:self.lockToYAxis];
創建一些樹
本文的代碼基于霧效果那篇文章的例子編寫,在ViewController.m
中添加下面的代碼生成一些樹木。因為我沒有判斷地形的高度,所以有些樹會只顯示一半,你也可以在生成樹的時候獲取對應坐標地形的高度進行判斷。
- (void)createTrees {
NSString *vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"vtx_billboard" ofType:@".glsl"];
NSString *fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"frag_billboard" ofType:@".glsl"];
self.treeGlContext = [GLContext contextWithVertexShaderPath:vertexShaderPath fragmentShaderPath:fragmentShaderPath];
for (int cycleTime = 0; cycleTime < 8; ++cycleTime) {
for (int angleSampleCount = 0; angleSampleCount < 9; ++angleSampleCount) {
float angle = rand() / (float)RAND_MAX * M_PI * 2.0;
float radius = rand() / (float)RAND_MAX * 70 + 40;
float xloc = cos(angle) * radius;
float zloc = sin(angle) * radius;
[self createTree: GLKVector3Make(xloc, 5, zloc)];
}
}
}
- (void)createTree:(GLKVector3)position {
GLKTextureInfo *grass = [GLKTextureLoader textureWithCGImage:[UIImage imageNamed:@"tree.png"].CGImage options:nil error:nil];
Billboard *tree = [[Billboard alloc] initWithGLContext:self.treeGlContext texture:grass];
[tree setBillboardCenterPosition:position];
[tree setBillboardSize:GLKVector2Make(6.0, 10.0)];
[tree setLockToYAxis:YES];
[self.objects addObject:tree];
}
總結
本文主要通過構建基于Billboards的樹來介紹這項技術,Billboards除了上面說到的用處之外,還可以用來渲染粒子系統中的粒子。這樣只需要兩個三角形就可以生成一個360度都可以完整看到的粒子了。