推荐:使用 NSDT场景编辑器快速搭建3D应用场景
建 模 和 3D 地形
大多数 3D 对象是 使用建模工具创建,这是有充分理由的。创建复杂对象 (如飞机甚至建筑物)很难在代码中完成。建模工具 几乎总是有意义的,但也有例外!其中之一可能是案例 就像飞行拱廊岛连绵起伏的丘陵一样。我们最终使用了 我们发现更简单,甚至可能更直观的技术:一个 高度图。
高度图是一种 使用常规二维图像来描述 像岛屿或其他地形一样的表面。这是一种非常常见的使用方式 高程数据,不仅在游戏中,而且在地理信息系统中 制图师和地质学家使用的 (GIS)。
帮助您获得想法 有关其工作原理,请查看此交互式演示中的高度图。尝试绘图 ,然后检出生成的地形。
![](https://file.jishuzhan.net/article/1699545495849930753/530f61e44c98484caa1e61b2a7cbf7da.png)
高度图背后的概念 很简单。在上图所示的图像中,纯黑色是 "地板"和纯白色是最高峰。介于两者之间的灰度颜色 表示相应的高程。这为我们提供了 256 个海拔高度,这 是我们游戏的大量细节。实际应用程序可能会使用完整的 色谱可存储更多层次的细节(2564 = 4,294,967,296 级 详细信息(如果包含 Alpha 通道)。
高度图有几个 与传统多边形网格相比的优势:
一、高度图很多 更紧凑。仅存储最重要的数据(高程)。它 需要以编程方式转换为 3D 对象,但这是 经典交易:您现在节省空间,稍后通过计算付款。通过存储 数据即图像,您将获得另一个空间优势:您可以利用标准 图像压缩技术并使数据变小(相比之下)!
其次,高度图是一个 生成、可视化和编辑地形的便捷方式。非常直观 当你看到一个。感觉有点像看地图。这被证明是 对飞行街机特别有用。我们设计和编辑了我们的岛屿 在 Photoshop 中!这使得根据需要进行小调整变得非常简单。 例如,当我们想确保跑道完全平坦时, 我们只是确保以单一颜色在该区域上绘画。
您可以看到高度图 下面的飞行拱廊。看看你是否能发现我们为 跑道和村庄。
![](https://file.jishuzhan.net/article/1699545495849930753/166f459c8d0e4209a17ae416fd849c42.png)
飞行街机岛的高度图。它是在Photoshop中创建的,它基于着名的太平洋岛链中的"大岛"。有什么猜测吗?
![](https://file.jishuzhan.net/article/1699545495849930753/3e3a5d170ed94e8b9ca550ddfcc673e2.jpeg)
在解码高度贴图后映射到生成的 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,但所有 使用全尺寸图像进行开发。这是来自 原始纹理。
![](https://file.jishuzhan.net/article/1699545495849930753/48a82ac27bd3496c87c141cebc21132d.png)
这些矩形表示 岛上城镇的建筑。我们很快注意到 我们可以在地形和 其他 3D 模型。即使使用我们巨大的岛屿纹理,区别在于 令人分心的明显!
为了解决这个问题,我们"混合" 以随机噪声的形式进入地形纹理的附加细节。您可以 请参阅下面的之前和之后。注意额外的噪点如何增强外观 地形细节。
![](https://file.jishuzhan.net/article/1699545495849930753/bcfc7649721548f988e8579073c9e4da.jpeg)
我们创建了一个自定义着色器 添加噪音。着色器为您提供了对 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 的创意效果类型 场景。在我们的案例中,这是画龙点名!