推荐:使用 NSDT场景编辑器快速搭建3D应用场景
建 模 和 3D 地形
大多数 3D 对象是 使用建模工具创建,这是有充分理由的。创建复杂对象 (如飞机甚至建筑物)很难在代码中完成。建模工具 几乎总是有意义的,但也有例外!其中之一可能是案例 就像飞行拱廊岛连绵起伏的丘陵一样。我们最终使用了 我们发现更简单,甚至可能更直观的技术:一个 高度图。
高度图是一种 使用常规二维图像来描述 像岛屿或其他地形一样的表面。这是一种非常常见的使用方式 高程数据,不仅在游戏中,而且在地理信息系统中 制图师和地质学家使用的 (GIS)。
帮助您获得想法 有关其工作原理,请查看此交互式演示中的高度图。尝试绘图 ,然后检出生成的地形。
高度图背后的概念 很简单。在上图所示的图像中,纯黑色是 “地板”和纯白色是最高峰。介于两者之间的灰度颜色 表示相应的高程。这为我们提供了 256 个海拔高度,这 是我们游戏的大量细节。实际应用程序可能会使用完整的 色谱可存储更多层次的细节(2564 = 4,294,967,296 级 详细信息(如果包含 Alpha 通道)。
高度图有几个 与传统多边形网格相比的优势:
一、高度图很多 更紧凑。仅存储最重要的数据(高程)。它 需要以编程方式转换为 3D 对象,但这是 经典交易:您现在节省空间,稍后通过计算付款。通过存储 数据即图像,您将获得另一个空间优势:您可以利用标准 图像压缩技术并使数据变小(相比之下)!
其次,高度图是一个 生成、可视化和编辑地形的便捷方式。非常直观 当你看到一个。感觉有点像看地图。这被证明是 对飞行街机特别有用。我们设计和编辑了我们的岛屿 在 Photoshop 中!这使得根据需要进行小调整变得非常简单。 例如,当我们想确保跑道完全平坦时, 我们只是确保以单一颜色在该区域上绘画。
您可以看到高度图 下面的飞行拱廊。看看你是否能发现我们为 跑道和村庄。
飞行街机岛的高度图。它是在Photoshop中创建的,它基于着名的太平洋岛链中的“大岛”。有什么猜测吗?
在解码高度贴图后映射到生成的 3D 网格上的纹理。更多内容见下文。
解码高度图
我们用Babylon.js建造了飞行拱廊,Babylon给了我们一个漂亮的 从高度图到 3D 的简单路径。Babylon提供了一个 API 来生成 来自高度图图像的网格几何体:
1 | var ground = BABYLON.Mesh.CreateGroundFromHeightMap( |
2 | |
3 | 'your-mesh-name', |
4 | |
5 | '/path/to/heightmap.png', |
6 | |
7 | 100, // width of the ground mesh (x axis) |
8 | |
9 | 100, // depth of the ground mesh (z axis) |
10 | |
11 | 40, // number of subdivisions |
12 | |
13 | 0, // min height |
14 | |
15 | 50, // max height |
16 | |
17 | scene, |
18 | |
19 | false, // updateable? |
20 | |
21 | null // callback when mesh is ready |
22 | |
23 | ); |
细节量是 由该细分的财产决定。需要注意的是, 参数是指高度图两侧的细分数量 图像,而不是单元格总数。所以稍微增加这个数字可以 对网格中的顶点总数有很大影响。
- 20 个细分 = 400 细胞
- 50 个细分 = 2,500 细胞
- 100 个细分 = 10,000 细胞
- 500 个细分 = 250,000 细胞
- 1,000 个细分 = 1,000,000 细胞
在下一节中,我们将 了解如何为地面设置纹理,但在尝试使用高度贴图时 创建时,查看线框很有用。这是应用简单代码 线框纹理,因此很容易看到高度图数据是如何转换为的 网格的顶点:
1 | // simple wireframe material |
2 | |
3 | var material = new BABYLON.StandardMaterial('ground-material', scene); |
4 | |
5 | material.wireframe = true; |
6 | |
7 | ground.material = material; |
创建纹理细节
一旦我们有一个模型,映射一个 质地相对简单。对于飞行街机,我们简单地创建了一个 非常大的图像,与我们的高度图中的岛屿相匹配。图像得到 延伸到地形的轮廓上,所以纹理和高度图 保持相关性。这真的很容易想象,再一次,所有 制作工作是在Photoshop中完成的。
原始纹理图像是 创建于 4096x4096。那可是挺大的!(我们最终将尺寸减小了 为了保持下载合理,级别到2048x2048,但所有 使用全尺寸图像进行开发。这是来自 原始纹理。
这些矩形表示 岛上城镇的建筑。我们很快注意到 我们可以在地形和 其他 3D 模型。即使使用我们巨大的岛屿纹理,区别在于 令人分心的明显!
为了解决这个问题,我们“混合” 以随机噪声的形式进入地形纹理的附加细节。您可以 请参阅下面的之前和之后。注意额外的噪点如何增强外观 地形细节。
我们创建了一个自定义着色器 添加噪音。着色器为您提供了对 WebGL 3D 场景的渲染,这是着色器如何 有用。
WebGL着色器由两个组成 主要部分:顶点和片段着色器。顶点的主要目标 着色器是将顶点映射到渲染帧中的某个位置。片段(或 像素)着色器控制像素的结果颜色。
着色器是用 称为GLSL(图形库着色器语言)的高级语言,它 类似于C。此代码在 GPU 上执行。深入了解如何 着色器工作,请参阅此处 有关如何为 Babylon.js 创建自己的自定义着色器的教程,或参阅此图形着色器编码初学者指南。
顶点着色器
我们不会改变我们的 纹理映射到地面网格体,因此我们的顶点着色器非常简单。 它只是计算标准映射并分配目标位置。
1 | precision mediump float; |
2 | |
3 | |
4 | |
5 | // Attributes |
6 | |
7 | attribute vec3 position; |
8 | |
9 | attribute vec3 normal; |
10 | |
11 | attribute vec2 uv; |
12 | |
13 | |
14 | |
15 | // Uniforms |
16 | |
17 | uniform mat4 worldViewProjection; |
18 | |
19 | |
20 | |
21 | // Varying |
22 | |
23 | varying vec4 vPosition; |
24 | |
25 | varying vec3 vNormal; |
26 | |
27 | varying vec2 vUV; |
28 | |
29 | |
30 | |
31 | void main() { |
32 | |
33 | |
34 | |
35 | vec4 p = vec4( position, 1.0 ); |
36 | |
37 | vPosition = p; |
38 | |
39 | vNormal = normal; |
40 | |
41 | vUV = uv; |
42 | |
43 | gl_Position = worldViewProjection * p; |
44 | |
45 | } |
碎片着色器
我们的片段着色器有点 更复杂。它结合了两个不同的图像:基础图像和混合图像。 基础图像映射到整个地面网格。在飞行街机中,这个 是岛屿的彩色图像。混合图像是使用的小噪点图像 在近距离为地面提供一些纹理和细节。着色器 组合每个图像中的值以创建跨 岛。
飞行的最后一课 街机发生在有雾的日子,所以我们的像素着色器的另一个任务是 调整颜色以模拟雾。调整基于顶点的距离 来自相机,远处像素被“遮挡”得更厉害 在雾中。您将在函数中看到此距离计算 在主着色器代码上方。calcFogFactor
1 | #ifdef GL_ES |
2 | |
3 | precision highp float; |
4 | |
5 | #endif |
6 | |
7 | |
8 | |
9 | uniform mat4 worldView; |
10 | |
11 | varying vec4 vPosition; |
12 | |
13 | varying vec3 vNormal; |
14 | |
15 | varying vec2 vUV; |
16 | |
17 | |
18 | |
19 | // Refs |
20 | |
21 | uniform sampler2D baseSampler; |
22 | |
23 | uniform sampler2D blendSampler; |
24 | |
25 | uniform float blendScaleU; |
26 | |
27 | uniform float blendScaleV; |
28 | |
29 | |
30 | |
31 | #define FOGMODE_NONE 0. |
32 | |
33 | #define FOGMODE_EXP 1. |
34 | |
35 | #define FOGMODE_EXP2 2. |
36 | |
37 | #define FOGMODE_LINEAR 3. |
38 | |
39 | #define E 2.71828 |
40 | |
41 | |
42 | |
43 | uniform vec4 vFogInfos; |
44 | |
45 | uniform vec3 vFogColor; |
46 | |
47 | |
48 | |
49 | float calcFogFactor() { |
50 | |
51 | |
52 | |
53 | // gets distance from camera to vertex |
54 | |
55 | float fogDistance = gl_FragCoord.z / gl_FragCoord.w; |
56 | |
57 | |
58 | |
59 | float fogCoeff = 1.0; |
60 | |
61 | float fogStart = vFogInfos.y; |
62 | |
63 | float fogEnd = vFogInfos.z; |
64 | |
65 | float fogDensity = vFogInfos.w; |
66 | |
67 | |
68 | |
69 | if (FOGMODE_LINEAR == vFogInfos.x) { |
70 | |
71 | fogCoeff = (fogEnd - fogDistance) / (fogEnd - fogStart); |
72 | |
73 | } |
74 | |
75 | else if (FOGMODE_EXP == vFogInfos.x) { |
76 | |
77 | fogCoeff = 1.0 / pow(E, fogDistance * fogDensity); |
78 | |
79 | } |
80 | |
81 | else if (FOGMODE_EXP2 == vFogInfos.x) { |
82 | |
83 | fogCoeff = 1.0 / pow(E, fogDistance * fogDistance * fogDensity * fogDensity); |
84 | |
85 | } |
86 | |
87 | |
88 | |
89 | return clamp(fogCoeff, 0.0, 1.0); |
90 | |
91 | } |
92 | |
93 | |
94 | |
95 | void main(void) { |
96 | |
97 | |
98 | |
99 | vec4 baseColor = texture2D(baseSampler, vUV); |
100 | |
101 | |
102 | |
103 | vec2 blendUV = vec2(vUV.x * blendScaleU, vUV.y * blendScaleV); |
104 | |
105 | vec4 blendColor = texture2D(blendSampler, blendUV); |
106 | |
107 | |
108 | |
109 | // multiply type blending mode |
110 | |
111 | vec4 color = baseColor * blendColor; |
112 | |
113 | |
114 | |
115 | // factor in fog color |
116 | |
117 | float fog = calcFogFactor(); |
118 | |
119 | color.rgb = fog * color.rgb + (1.0 - fog) * vFogColor; |
120 | |
121 | |
122 | |
123 | gl_FragColor = color; |
124 | |
125 | } |
我们定制的最后一件作品 Blend shader 是 Babylon 使用的 JavaScript 代码。主要目的 此代码用于准备传递给顶点和像素着色器的参数。
1 | function BlendMaterial(name, scene, options) { |
2 | |
3 | this.name = name; |
4 | |
5 | this.id = name; |
6 | |
7 | |
8 | |
9 | this.options = options; |
10 | |
11 | this.blendScaleU = options.blendScaleU || 1; |
12 | |
13 | this.blendScaleV = options.blendScaleV || 1; |
14 | |
15 | |
16 | |
17 | this._scene = scene; |
18 | |
19 | scene.materials.push(this); |
20 | |
21 | |
22 | |
23 | var assets = options.assetManager; |
24 | |
25 | var textureTask = assets.addTextureTask('blend-material-base-task', options.baseImage); |
26 | |
27 | textureTask.onSuccess = _.bind(function(task) { |
28 | |
29 | |
30 | |
31 | this.baseTexture = task.texture; |
32 | |
33 | this.baseTexture.uScale = 1; |
34 | |
35 | this.baseTexture.vScale = 1; |
36 | |
37 | |
38 | |
39 | if (options.baseHasAlpha) { |
40 | |
41 | this.baseTexture.hasAlpha = true; |
42 | |
43 | } |
44 | |
45 | |
46 | |
47 | }, this); |
48 | |
49 | |
50 | |
51 | textureTask = assets.addTextureTask('blend-material-blend-task', options.blendImage); |
52 | |
53 | textureTask.onSuccess = _.bind(function(task) { |
54 | |
55 | this.blendTexture = task.texture; |
56 | |
57 | this.blendTexture.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE; |
58 | |
59 | this.blendTexture.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE; |
60 | |
61 | }, this); |
62 | |
63 | |
64 | |
65 | } |
66 | |
67 | |
68 | |
69 | BlendMaterial.prototype = Object.create(BABYLON.Material.prototype); |
70 | |
71 | |
72 | |
73 | BlendMaterial.prototype.needAlphaBlending = function () { |
74 | |
75 | return (this.options.baseHasAlpha === true); |
76 | |
77 | }; |
78 | |
79 | |
80 | |
81 | BlendMaterial.prototype.needAlphaTesting = function () { |
82 | |
83 | return false; |
84 | |
85 | }; |
86 | |
87 | |
88 | |
89 | BlendMaterial.prototype.isReady = function (mesh) { |
90 | |
91 | var engine = this._scene.getEngine(); |
92 | |
93 | |
94 | |
95 | // make sure textures are ready |
96 | |
97 | if (!this.baseTexture || !this.blendTexture) { |
98 | |
99 | return false; |
100 | |
101 | } |
102 | |
103 | |
104 | |
105 | if (!this._effect) { |
106 | |
107 | this._effect = engine.createEffect( |
108 | |
109 | |
110 | |
111 | // shader name |
112 | |
113 | "blend", |
114 | |
115 | |
116 | |
117 | // attributes describing topology of vertices |
118 | |
119 | [ "position", "normal", "uv" ], |
120 | |
121 | |
122 | |
123 | // uniforms (external variables) defined by the shaders |
124 | |
125 | [ "worldViewProjection", "world", "blendScaleU", "blendScaleV", "vFogInfos", "vFogColor" ], |
126 | |
127 | |
128 | |
129 | // samplers (objects used to read textures) |
130 | |
131 | [ "baseSampler", "blendSampler" ], |
132 | |
133 | |
134 | |
135 | // optional define string |
136 | |
137 | ""); |
138 | |
139 | } |
140 | |
141 | |
142 | |
143 | if (!this._effect.isReady()) { |
144 | |
145 | return false; |
146 | |
147 | } |
148 | |
149 | |
150 | |
151 | return true; |
152 | |
153 | }; |
154 | |
155 | |
156 | |
157 | BlendMaterial.prototype.bind = function (world, mesh) { |
158 | |
159 | |
160 | |
161 | var scene = this._scene; |
162 | |
163 | this._effect.setFloat4("vFogInfos", scene.fogMode, scene.fogStart, scene.fogEnd, scene.fogDensity); |
164 | |
165 | this._effect.setColor3("vFogColor", scene.fogColor); |
166 | |
167 | |
168 | |
169 | this._effect.setMatrix("world", world); |
170 | |
171 | this._effect.setMatrix("worldViewProjection", world.multiply(scene.getTransformMatrix())); |
172 | |
173 | |
174 | |
175 | // Textures |
176 | |
177 | this._effect.setTexture("baseSampler", this.baseTexture); |
178 | |
179 | this._effect.setTexture("blendSampler", this.blendTexture); |
180 | |
181 | |
182 | |
183 | this._effect.setFloat("blendScaleU", this.blendScaleU); |
184 | |
185 | this._effect.setFloat("blendScaleV", this.blendScaleV); |
186 | |
187 | }; |
188 | |
189 | |
190 | |
191 | BlendMaterial.prototype.dispose = function () { |
192 | |
193 | |
194 | |
195 | if (this.baseTexture) { |
196 | |
197 | this.baseTexture.dispose(); |
198 | |
199 | } |
200 | |
201 | |
202 | |
203 | if (this.blendTexture) { |
204 | |
205 | this.blendTexture.dispose(); |
206 | |
207 | } |
208 | |
209 | |
210 | |
211 | this.baseDispose(); |
212 | |
213 | }; |
Babylon.js使它变得容易 创建基于着色器的自定义材质。我们的混合材料相对简单, 但它确实对岛屿的外观产生了很大的影响,当 飞机低空飞到地面。着色器将 GPU 的强大功能带到 浏览器,扩展可应用于 3D 的创意效果类型 场景。在我们的案例中,这是画龙点名!
原文链接:使用 WebGL 为 HTML5 游戏创建逼真的地形 (mvrlink.com)