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

推荐:使用 NSDT场景编辑器快速搭建3D应用场景

建 模 和 3D 地形

大多数 3D 对象是 使用建模工具创建,这是有充分理由的。创建复杂对象 (如飞机甚至建筑物)很难在代码中完成。建模工具 几乎总是有意义的,但也有例外!其中之一可能是案例 就像飞行拱廊岛连绵起伏的丘陵一样。我们最终使用了 我们发现更简单,甚至可能更直观的技术:一个 高度图。

高度图是一种 使用常规二维图像来描述 像岛屿或其他地形一样的表面。这是一种非常常见的使用方式 高程数据,不仅在游戏中,而且在地理信息系统中 制图师和地质学家使用的 (GIS)。

帮助您获得想法 有关其工作原理,请查看此交互式演示中的高度图。尝试绘图 ,然后检出生成的地形。

使用 WebGL 为 HTML5 游戏创建逼真的地形_第1张图片

高度图背后的概念 很简单。在上图所示的图像中,纯黑色是 “地板”和纯白色是最高峰。介于两者之间的灰度颜色 表示相应的高程。这为我们提供了 256 个海拔高度,这 是我们游戏的大量细节。实际应用程序可能会使用完整的 色谱可存储更多层次的细节(2564 = 4,294,967,296 级 详细信息(如果包含 Alpha 通道)。

高度图有几个 与传统多边形网格相比的优势:

一、高度图很多 更紧凑。仅存储最重要的数据(高程)。它 需要以编程方式转换为 3D 对象,但这是 经典交易:您现在节省空间,稍后通过计算付款。通过存储 数据即图像,您将获得另一个空间优势:您可以利用标准 图像压缩技术并使数据变小(相比之下)!

其次,高度图是一个 生成、可视化和编辑地形的便捷方式。非常直观 当你看到一个。感觉有点像看地图。这被证明是 对飞行街机特别有用。我们设计和编辑了我们的岛屿 在 Photoshop 中!这使得根据需要进行小调整变得非常简单。 例如,当我们想确保跑道完全平坦时, 我们只是确保以单一颜色在该区域上绘画。

您可以看到高度图 下面的飞行拱廊。看看你是否能发现我们为 跑道和村庄。

使用 WebGL 为 HTML5 游戏创建逼真的地形_第2张图片

飞行街机岛的高度图。它是在Photoshop中创建的,它基于着名的太平洋岛链中的“大岛”。有什么猜测吗?

使用 WebGL 为 HTML5 游戏创建逼真的地形_第3张图片

在解码高度贴图后映射到生成的 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,但所有 使用全尺寸图像进行开发。这是来自 原始纹理。

使用 WebGL 为 HTML5 游戏创建逼真的地形_第4张图片

这些矩形表示 岛上城镇的建筑。我们很快注意到 我们可以在地形和 其他 3D 模型。即使使用我们巨大的岛屿纹理,区别在于 令人分心的明显!

为了解决这个问题,我们“混合” 以随机噪声的形式进入地形纹理的附加细节。您可以 请参阅下面的之前和之后。注意额外的噪点如何增强外观 地形细节。

使用 WebGL 为 HTML5 游戏创建逼真的地形_第5张图片

我们创建了一个自定义着色器 添加噪音。着色器为您提供了对 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)

你可能感兴趣的:(webgl,html5,游戏)