使用 WebGL 为 HTML5 游戏创建逼真的地形
在本文中,我将分享我们应对这些有趣挑战之一的方法:一种创建逼真的大型地形的简单方法。
![使用 WebGL 为 HTML5 游戏创建逼真的地形](/content/images/size/w2000/2023/08/2-11.png)
推荐:使用NSDT场景编辑器快速搭建3D应用场景
建 模 和 3D 地形
大多数 3D 对象是 使用建模工具创建,这是有充分理由的。创建复杂对象 (如飞机甚至建筑物)很难在代码中完成。建模工具 几乎总是有意义的,但也有例外!其中之一可能是案例 就像飞行拱廊岛连绵起伏的丘陵一样。我们最终使用了 我们发现更简单,甚至可能更直观的技术:一个 高度图。
高度图是一种 使用常规二维图像来描述 像岛屿或其他地形一样的表面。这是一种非常常见的使用方式 高程数据,不仅在游戏中,而且在地理信息系统中 制图师和地质学家使用的 (GIS)。
帮助您获得想法 有关其工作原理,请查看此交互式演示中的高度图。尝试绘图 ,然后检出生成的地形。
![高度图演示的屏幕截图](https://cms-assets.tutsplus.com/cdn-cgi/image/width=600/uploads/users/48/posts/24539/image/heightmap.png)
高度图背后的概念 很简单。在上图所示的图像中,纯黑色是 “地板”和纯白色是最高峰。介于两者之间的灰度颜色 表示相应的高程。这为我们提供了 256 个海拔高度,这 是我们游戏的大量细节。实际应用程序可能会使用完整的 色谱可存储更多层次的细节(2564 = 4,294,967,296 级 详细信息(如果包含 Alpha 通道)。
高度图有几个 与传统多边形网格相比的优势:
一、高度图很多 更紧凑。仅存储最重要的数据(高程)。它 需要以编程方式转换为 3D 对象,但这是 经典交易:您现在节省空间,稍后通过计算付款。通过存储 数据即图像,您将获得另一个空间优势:您可以利用标准 图像压缩技术并使数据变小(相比之下)!
其次,高度图是一个 生成、可视化和编辑地形的便捷方式。非常直观 当你看到一个。感觉有点像看地图。这被证明是 对飞行街机特别有用。我们设计和编辑了我们的岛屿 在 Photoshop 中!这使得根据需要进行小调整变得非常简单。 例如,当我们想确保跑道完全平坦时, 我们只是确保以单一颜色在该区域上绘画。
您可以看到高度图 下面的飞行拱廊。看看你是否能发现我们为 跑道和村庄。
![飞行街机的高度图](https://cms-assets.tutsplus.com/cdn-cgi/image/width=600/uploads/users/48/posts/24539/image/island-height.png)
![](https://cms-assets.tutsplus.com/cdn-cgi/image/width=600/uploads/users/48/posts/24539/image/island.jpg)
解码高度图
我们用Babylon.js建造了飞行拱廊,Babylon给了我们一个漂亮的 从高度图到 3D 的简单路径。Babylon提供了一个 API 来生成 来自高度图图像的网格几何体:
ar ground = BABYLON.Mesh.CreateGroundFromHeightMap(
'your-mesh-name',
'/path/to/heightmap.png',
100, // width of the ground mesh (x axis)
100, // depth of the ground mesh (z axis)
40, // number of subdivisions
0, // min height
50, // max height
scene,
false, // updateable?
null // callback when mesh is ready
);`
细节量是 由该细分的财产决定。需要注意的是, 参数是指高度图两侧的细分数量 图像,而不是单元格总数。所以稍微增加这个数字可以 对网格中的顶点总数有很大影响。
- 20 个细分 = 400 细胞
- 50 个细分 = 2,500 细胞
- 100 个细分 = 10,000 细胞
- 500 个细分 = 250,000 细胞
- 1,000 个细分 = 1,000,000 细胞
在下一节中,我们将 了解如何为地面设置纹理,但在尝试使用高度贴图时 创建时,查看线框很有用。这是应用简单代码 线框纹理,因此很容易看到高度图数据是如何转换为的 网格的顶点:
// simple wireframe material
var material = new BABYLON.StandardMaterial('ground-material', scene);
material.wireframe = true;
ground.material = material;`
创建纹理细节
一旦我们有一个模型,映射一个 质地相对简单。对于飞行街机,我们简单地创建了一个 非常大的图像,与我们的高度图中的岛屿相匹配。图像得到 延伸到地形的轮廓上,所以纹理和高度图 保持相关性。这真的很容易想象,再一次,所有 制作工作是在Photoshop中完成的。
原始纹理图像是 创建于 4096x4096。那可是挺大的!(我们最终将尺寸减小了 为了保持下载合理,级别到2048x2048,但所有 使用全尺寸图像进行开发。这是来自 原始纹理。
![原始岛屿纹理的全像素示例](https://cms-assets.tutsplus.com/cdn-cgi/image/width=600/uploads/users/48/posts/24539/image/island-texture.jpg)
这些矩形表示 岛上城镇的建筑。我们很快注意到 我们可以在地形和 其他 3D 模型。即使使用我们巨大的岛屿纹理,区别在于 令人分心的明显!
为了解决这个问题,我们“混合” 以随机噪声的形式进入地形纹理的附加细节。您可以 请参阅下面的之前和之后。注意额外的噪点如何增强外观 地形细节。
![机场纹理前后对比](https://cms-assets.tutsplus.com/cdn-cgi/image/width=600/uploads/users/48/posts/24539/image/airport-texture.jpg)
我们创建了一个自定义着色器 添加噪音。着色器为您提供了对 WebGL 3D 场景的渲染,这是着色器如何 有用。
WebGL着色器由两个组成 主要部分:顶点和片段着色器。顶点的主要目标 着色器是将顶点映射到渲染帧中的某个位置。片段(或 像素)着色器控制像素的结果颜色。
着色器是用 称为GLSL(图形库着色器语言)的高级语言,它 类似于C。此代码在 GPU 上执行。深入了解如何 着色器工作,请参阅此处 有关如何为 Babylon.js 创建自己的自定义着色器的教程,或参阅此图形着色器编码初学者指南。
顶点着色器
我们不会改变我们的 纹理映射到地面网格体,因此我们的顶点着色器非常简单。 它只是计算标准映射并分配目标位置。
precision mediump float;
// Attributes
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
// Uniforms
uniform mat4 worldViewProjection;
// Varying
varying vec4 vPosition;
varying vec3 vNormal;
varying vec2 vUV;
void main() {
vec4 p = vec4( position, 1.0 );
vPosition = p;
vNormal = normal;
vUV = uv;
gl_Position = worldViewProjection * p;
}
碎片着色器
我们的片段着色器有点 更复杂。它结合了两个不同的图像:基础图像和混合图像。 基础图像映射到整个地面网格。在飞行街机中,这个 是岛屿的彩色图像。混合图像是使用的小噪点图像 在近距离为地面提供一些纹理和细节。着色器 组合每个图像中的值以创建跨 岛。
飞行的最后一课 街机发生在有雾的日子,所以我们的像素着色器的另一个任务是 调整颜色以模拟雾。调整基于顶点的距离 来自相机,远处像素被“遮挡”得更厉害 在雾中。您将在函数中看到此距离计算 在主着色器代码上方。calcFogFactor
// #ifdef GL_ES
precision highp float;
// #endif
uniform mat4 worldView;
varying vec4 vPosition;
varying vec3 vNormal;
varying vec2 vUV;
// Refs
uniform sampler2D baseSampler;
uniform sampler2D blendSampler;
uniform float blendScaleU;
uniform float blendScaleV;
// #define FOGMODE_NONE 0.
// #define FOGMODE_EXP 1.
// #define FOGMODE_EXP2 2.
// #define FOGMODE_LINEAR 3.
// #define E 2.71828
uniform vec4 vFogInfos;
uniform vec3 vFogColor;
float calcFogFactor() {
// gets distance from camera to vertex
float fogDistance = gl_FragCoord.z / gl_FragCoord.w;
float fogCoeff = 1.0;
float fogStart = vFogInfos.y;
float fogEnd = vFogInfos.z;
float fogDensity = vFogInfos.w;
if (FOGMODE_LINEAR == vFogInfos.x) {
fogCoeff = (fogEnd - fogDistance) / (fogEnd - fogStart);
}
else if (FOGMODE_EXP == vFogInfos.x) {
fogCoeff = 1.0 / pow(E, fogDistance * fogDensity);
}
else if (FOGMODE_EXP2 == vFogInfos.x) {
fogCoeff = 1.0 / pow(E, fogDistance * fogDistance * fogDensity * fogDensity);
}
return clamp(fogCoeff, 0.0, 1.0);
}
void main(void) {
vec4 baseColor = texture2D(baseSampler, vUV);
vec2 blendUV = vec2(vUV.x * blendScaleU, vUV.y * blendScaleV);
vec4 blendColor = texture2D(blendSampler, blendUV);
// multiply type blending mode
vec4 color = baseColor * blendColor;
// factor in fog color
float fog = calcFogFactor();
color.rgb = fog * color.rgb + (1.0 - fog) * vFogColor;
gl_FragColor = color;
}
我们定制的最后一件作品 Blend shader 是 Babylon 使用的 JavaScript 代码。主要目的 此代码用于准备传递给顶点和像素着色器的参数。
function BlendMaterial(name, scene, options) {
this.name = name;
this.id = name;
this.options = options;
this.blendScaleU = options.blendScaleU || 1;
this.blendScaleV = options.blendScaleV || 1;
this._scene = scene;
scene.materials.push(this);
var assets = options.assetManager;
var textureTask = assets.addTextureTask('blend-material-base-task', options.baseImage);
textureTask.onSuccess = _.bind(function(task) {
this.baseTexture = task.texture;
this.baseTexture.uScale = 1;
this.baseTexture.vScale = 1;
if (options.baseHasAlpha) {
this.baseTexture.hasAlpha = true;
}
}, this);
textureTask = assets.addTextureTask('blend-material-blend-task', options.blendImage);
textureTask.onSuccess = _.bind(function(task) {
this.blendTexture = task.texture;
this.blendTexture.wrapU = BABYLON.Texture.MIRROR_ADDRESSMODE;
this.blendTexture.wrapV = BABYLON.Texture.MIRROR_ADDRESSMODE;
}, this);
}
BlendMaterial.prototype = Object.create(BABYLON.Material.prototype);
BlendMaterial.prototype.needAlphaBlending = function () {
return (this.options.baseHasAlpha === true);
};
BlendMaterial.prototype.needAlphaTesting = function () {
return false;
};
BlendMaterial.prototype.isReady = function (mesh) {
var engine = this._scene.getEngine();
// make sure textures are ready
if (!this.baseTexture || !this.blendTexture) {
return false;
}
if (!this._effect) {
this._effect = engine.createEffect(
// shader name
"blend",
// attributes describing topology of vertices
[ "position", "normal", "uv" ],
// uniforms (external variables) defined by the shaders
[ "worldViewProjection", "world", "blendScaleU", "blendScaleV", "vFogInfos", "vFogColor" ],
// samplers (objects used to read textures)
[ "baseSampler", "blendSampler" ],
// optional define string
"");
}
if (!this._effect.isReady()) {
return false;
}
return true;
};
BlendMaterial.prototype.bind = function (world, mesh) {
var scene = this._scene;
this._effect.setFloat4("vFogInfos", scene.fogMode, scene.fogStart, scene.fogEnd, scene.fogDensity);
this._effect.setColor3("vFogColor", scene.fogColor);
this._effect.setMatrix("world", world);
this._effect.setMatrix("worldViewProjection", world.multiply(scene.getTransformMatrix()));
// Textures
this._effect.setTexture("baseSampler", this.baseTexture);
this._effect.setTexture("blendSampler", this.blendTexture);
this._effect.setFloat("blendScaleU", this.blendScaleU);
this._effect.setFloat("blendScaleV", this.blendScaleV);
};
BlendMaterial.prototype.dispose = function () {
if (this.baseTexture) {
this.baseTexture.dispose();
}
if (this.blendTexture) {
this.blendTexture.dispose();
}
this.baseDispose();
};
Babylon.js使它变得容易 创建基于着色器的自定义材质。我们的混合材料相对简单, 但它确实对岛屿的外观产生了很大的影响,当 飞机低空飞到地面。着色器将 GPU 的强大功能带到 浏览器,扩展可应用于 3D 的创意效果类型 场景。在我们的案例中,这是画龙点名!
由3D建模学习工作室 整理翻译,转载请注明出处!