Create Realistic Terrains for HTML5 Games Using WebGL

Recommendation: Use the NSDT scene editor to quickly build a 3D application scene

Modeling and 3D Terrain

Most 3D objects are created using modeling tools, and for good reason. Creating complex objects like airplanes or even buildings is hard to do in code. Modeling tools almost always make sense, but there are exceptions! One of them may be that the case is like the rolling hills of the flying arcade island. We ended up using a technique we found simpler, and possibly even more intuitive: a heightmap.

A heightmap is a method that uses a regular two-dimensional image to describe a surface like an island or other terrain. This is a very common way to use elevation data, not only in games but also in geographic information systems (GIS) used by cartographers and geologists.

To help you get an idea of ​​how it works, check out the heightmap in this interactive demo. Try plotting, then check out the resulting terrain.

The concept behind heightmaps is simple. In the image shown above, pure black is the "floor" and pure white is the highest peak. Grayscale colors in between indicate corresponding elevations. This gives us 256 altitudes, which is a lot of detail for our game. A real application might use the full color spectrum to store many more levels of detail (2564 = 4,294,967,296 levels of detail if the alpha channel is included).

Heightmaps have several advantages over traditional polygonal meshes:

1. The height map is much more compact. Only the most important data (elevation) is stored. It needs to be converted to a 3D object programmatically, but it's a classic deal: you save space now, pay for it later by computing. By storing the data as images, you get another space advantage: you can take advantage of standard image compression techniques and keep the data small (by comparison)!

Second, heightmaps are a convenient way to generate, visualize, and edit terrain. Very intuitive when you see one. It feels a bit like looking at a map. This proved to be especially useful for flying arcade machines. We designed and edited our island in Photoshop! This makes it very simple to make small adjustments as needed. For example, when we want to make sure the runway is completely flat, we just make sure to paint over that area with a single color.

You can see the flying arcade below the height map. See if you can spot us for the runways and villages.

Heightmap for Flying Arcade

Heightmap of the flying arcade island. It was created in Photoshop and it is based on the "Big Island" in the famous Pacific island chain. Any guesses?

The texture to map onto the resulting 3D mesh after decoding the heightmap. Read more below.

decode heightmap

We built flying arcades with Babylon.js, and Babylon gave us a nice easy path from heightmaps to 3D. Babylon provides an API to generate mesh geometry from heightmap images:

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
);

The amount of detail is determined by the property of the subdivision. It should be noted that the parameter refers to the number of subdivisions on both sides of the heightmap image, not the total number of cells. So increasing this number slightly can have a big impact on the total number of vertices in the mesh.

  • 20 segments = 400 cells
  • 50 subdivisions = 2,500 cells
  • 100 segments = 10,000 cells
  • 500 segments = 250,000 cells
  • 1,000 segments = 1,000,000 cells

We'll see how to texture the ground in the next section, but it's useful to look at the wireframe when trying to create with heightmaps. Here's simple code to apply a wireframe texture, so it's easy to see how the heightmap data translates to the vertices of the mesh:

1
// simple wireframe material 
2
 
3
var material = new BABYLON.StandardMaterial('ground-material', scene);
4
 
5
material.wireframe = true;
6
 
7
ground.material = material;

Create texture details

Once we have a model, mapping a texture is relatively simple. For the flying arcade, we simply created a very large image that matched the islands in our heightmap. The imagery gets stretched over the contours of the terrain, so textures and heightmaps remain associative. It's really easy to visualize, and again, all production work is done in Photoshop.

The original texture image was created at 4096x4096. That's pretty big! (We ended up reducing the size to 2048x2048 in order to keep the downloads reasonable, but all using full size images for development. This is from the original texture.

A full pixel example of the original island texture.  The whole town is only about 300 square pixels.

These rectangles represent the buildings of the towns on the island. We quickly noticed that we could use terrain and other 3D models. Even with our huge island textures, the difference is distractingly obvious!

To solve this, we "blend" additional detail into the terrain texture in the form of random noise. You can see the before and after below. Notice how the extra noise enhances the apparent terrain detail.

Airport texture before and after comparison

We created a custom shader to add noise. Shaders give you a rendering of a WebGL 3D scene, and this is how shaders are useful.

WebGL shaders consist of two main parts: vertex and fragment shaders. The main goal of a vertex shader is to map a vertex to a position in the rendered frame. A fragment (or pixel) shader controls the resulting color of a pixel.

Shaders are written in a high-level language called GLSL (Graphics Library Shader Language), which is similar to C. This code executes on the GPU. For an in-depth look at how shaders work, see our tutorial on how to create your own custom shaders for Babylon.js here, or see this beginner's guide to graphics shader coding.

vertex shader

We won't change our texture mapping to the ground mesh, so our vertex shader is very simple. It just calculates the standard mapping and assigns the target location.

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
}

Fragment shader

Our fragment shader is a bit more complicated. It combines two different images: a base image and a blended image. The base image is mapped to the entire ground grid. In Flying Arcade, this is a color image of the island. Blended images are small noise images used at close range to give the ground some texture and detail. The shader combines the values ​​in each image to create the span islands.

飞行的最后一课 街机发生在有雾的日子,所以我们的像素着色器的另一个任务是 调整颜色以模拟雾。调整基于顶点的距离 来自相机,远处像素被“遮挡”得更厉害 在雾中。您将在函数中看到此距离计算 在主着色器代码上方。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 makes it easy to create custom shader-based materials. Our mix of materials was relatively simple, but it did make a big difference to the look of the island as the plane flew low to the ground. Shaders bring the power of the GPU to the browser, extending the scene of the types of creative effects that can be applied to 3D. In our case, it's the dragon roll call!

Original Link: Create Realistic Terrain for HTML5 Games Using WebGL (mvrlink.com)

Guess you like

Origin blog.csdn.net/ygtu2018/article/details/132713390