使用 WebGL 为 HTML5 游戏创建逼真的地形

推荐:使用 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,但所有 使用全尺寸图像进行开发。这是来自 原始纹理。

原始岛屿纹理的全像素示例。整个城镇只有大约300平方像素。

这些矩形表示 岛上城镇的建筑。我们很快注意到 我们可以在地形和 其他 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)

猜你喜欢

转载自blog.csdn.net/ygtu2018/article/details/132713390