受到一些很棒的 three.js 演示、与 covid 相关的旅行禁令以及可能在 pinterest 上花太多时间看美丽的旅行照片的启发——我开始看看我是否可以使用 three.js 和r3f在浏览器中渲染一个令人信服的风景场景。
推荐:将 NSDT场景编辑器 加入你的3D开发工具链。
在过去一个月左右的时间里,我一直在尝试不同的方法,并在互联网上搜索有关如何使用浏览器技术渲染半现实景观的技巧。 我发现它很有价值,但也比我预期的要难得多。
我整理了一份简短指南,总结了我从 A 到 B 所使用的技巧和技巧:
TLDR:
我使用特定于地形的自定义实用程序扩展了 three.js 标准材质。 这种经过修改的材质使用多种纹理来为地形着色、塑造和照亮地形。 我对这些纹理中的一些进行平铺和分层,以确保地形在更大的观看距离范围内看起来不错。
我选择从一个名为 world creator 的图形用户界面生成和导出地形数据。在线演示可以访问这里,源码托管在GitHub。
我将这篇文章结构化为我用来改进渲染的技术列表,而不是冗长的教程。 对于每一种技术,我都会提供一个简短的描述,如果你不熟悉它,这应该可以帮助你定位。 我还将提供一个提示/技巧部分,该部分应该阐明我如何在示例场景中使用该技术。 我希望这将使它对不同技能水平的人更容易浏览和有用。
动机限制:
工具:
React + r3f 并不是绝对必要的,但将这些概念应用到 vanilla three.js 设置中应该很容易。 另请注意,World creator 不是免费的,但有很多替代方法可以生成高度图等。
高度图将地形的垂直高度编码为从 0(黑色)到 1(白色)的像素值。 Three.js 内置处理位移的代码(顶点着色器)。 位移贴图(displacement map)可用于 3d 对象,但对于风景,我只使用了一个简单的平面。 最主要的是确保你的比例与生成高度图的任何程序或工具相匹配。 可以使用 displacementScale 和 displacementBias 来使 three.js 的比例匹配。
高度图是这样的图像:
提示:对于高度图中的每个像素,你至少需要对平面进行一次细分,否则three.js 着色器将不会有一个关联的顶点来定位。
技巧:除非相机靠近地面,否则高度图不需要特别高分辨率。 在上面的示例中,我使用了 1024*1024 高度图。 使地形看起来不错的大部分细节都来自法线贴图和漫反射贴图。
限制:2d 高度图无法表示 3d 地形细节,如洞穴或悬垂。
过度简化天空“是蓝色的”,所以当你看它实际上把它背后的东西染成蓝色。 雾在 three.js 场景中是一种常见的技术,但它在风景中尤为重要,因为它可以帮助观众理解山丘、树木等的规模和排列:
注意:使用的材质需要知道如何正确渲染雾。 如果你不扩展 three.js 材质,这将不会自动工作。
HDRI(高动态范围图像)是一种环绕场景的大纹理,可作为 PBR 材质的更逼真光源。 我喜欢光线具有更自然的方向和颜色,但找到适合我的场景的光线有点费力。 也就是说,我认为这是值得的,因为照明设置是对场景质量和气氛影响最大的因素之一。
// I used the Environment component from Drei with one of the preset HDRIs
import { Environment } from "@react-three/drei"
export default Landscape(){
return (
// landape mesh ect
...
// hdri
)
}
提示:
技巧:尝试使用标准网格材质的 normalScale、 envMapIntensity 和 metalness 参数。 它们使你可以对对比度、颜色深度和亮度进行大量控制。
对于大多数 3d 对象,我们使用称为漫反射纹理的单一纹理来应用颜色。 如果我们放大到接近地形大小的对象,这很快就会崩溃,因为每次将其尺寸加倍时,图像的文件大小/内存影响不会线性缩放。 在地形大小的对象上应用单个漫反射纹理会变得模糊或过大。
从逻辑上讲,我们应该能够通过混合和重复多个较小的纹理来解决这个问题,每个纹理对应一种地形类型(草地、泥地、岩石等)。
纹理splat是一种方法。 它的工作原理是获取多个纹理并将它们与另一个称为 splat 纹理的纹理中的颜色通道相关联。 splat 纹理中的一个像素被渲染为 100x100 图像 - 取决于我们的纹理大小和它们重复的次数 - 使得 splat 纹理明显小于相同细节级别的漫反射纹理。
例如,如果 splat 中的像素是纯红色,那么我们将该区域渲染为泥土,如果它是纯绿色,则将其渲染为草地,如果它是红色和绿色的混合,那么我们可以线性组合瓷砖纹理的 rgb 值以 更自然地融合在草地和泥泞地区之间。 混合对于防止它看起来像 Minecraft 地图至关重要。
// inside the main function of the fragment shader ...
vec4 diffuse1 = texture2D(uDiffuse1, uv * 100.0);
vec4 diffuse2 = texture2D(uDiffuse2, uv * 100.0);
vec4 splat1 = texture2D(uSplat1, uv);
vec4 color = diffuse1 * splat1.r + diffuse2 * splat1.g;
diffuseColor = vec4( color.rgb, opacity );
提示:World creator 以 TGA 格式导出 splat 纹理,我以前从未听说过这种格式,并且发现使用起来有些困难,但幸运的是,Three.js 示例包含一个 TGA 加载器,可以处理将 .tga 文件加载到数据纹理中。
局限性:纹理平铺会导致明显的纹理重复,我们将在下面解决:
虽然这样做很容易,但没有什么要求我们将纹理放置在会产生不良平铺效果的网格图案类型中。 Inigo Quilez 有一篇关于修复纹理重复的多种方法的好文章,我在我的场景中使用了它。
简单的说,该技术本质上归结为使用随机噪声纹理和一些数学来更有机地在网格表面上无缝地放置和混合纹理。
// use instead of texture2D
vec4 textureNoTile( sampler2D samp, vec2 uv ){
// sample variation pattern
float k = texture2D( uNoise, 0.005*uv ).x; // cheap (cache friendly) lookup
// compute index
float l = k*8.0;
float f = fract(l);
float ia = floor(l);
float ib = ia + 1.0;
// offsets for the different virtual patterns
float v = 0.4;
vec2 offa = sin(vec2(3.0,7.0)*ia); // can replace with any other hash
vec2 offb = sin(vec2(3.0,7.0)*ib); // can replace with any other hash
// compute derivatives for mip-mapping
vec2 dx = dFdx(uv), dy = dFdy(uv);
// sample the two closest virtual patterns
vec3 cola = texture2DGradEXT( samp, uv + v*offa, dx, dy ).xyz;
vec3 colb = texture2DGradEXT( samp, uv + v*offb, dx, dy ).xyz;
// // interpolate between the two virtual patterns
vec3 col = mix( cola, colb, smoothstep(0.2,0.8,f-0.1*sum(cola-colb)) );
return vec4(col,1.0);
}
他们让我们在给定点对表面的“正常”角度进行编码。 这有点 hack,但它有助于确定从该物体反射到相机的光量,使我们能够平滑或创建额外的细节,而无需渲染额外的三角形。 同样,这是 THREE.js 物理材质中内置的内容。
提示:在岩石等细节物体上使用法线贴图比使用你能找到的最高分辨率漫反射纹理重要得多。
World Creator 导出整个地形的法线贴图,但也导出我们在景观中平铺的岩石纹理的法线贴图:
理想情况下,场景将受益于宏观悬崖面法线以及水平法线的更细微的凹凸和裂缝。 不幸的是,如果只是将法线纹理添加在一起,它们会变得浑浊和/或在某些地方会出现超级亮点或暗点。
Stephen Hill 对问题和可能的解决方案有很好的总结 , 它给着色器增加了一些复杂性,但在我看来这是值得的:
// adapted from original article so more than two normals can be blended
vec4 blend_normals(vec4 n1, vec4 n2){
vec3 t = n1.xyz*vec3( 2, 2, 2) + vec3(-1, -1, 0);
vec3 u = n2.xyz*vec3(-2, -2, 2) + vec3( 1, 1, -1);
vec3 r = t*dot(t, u) /t.z -u;
return vec4((r), 1.0) * 0.5 + 0.5;
}
//usage:
vec4 blend = blend_normals(n1, n2);
vec4 triblend = blend_normals(blend, n3);
提示:当以不同的比例重复时,使用多个法线往往效果最好。 尽管上述技术在数学上比简单的线性组合更正确,但我发现应用多个相同大小的法线最终看起来就像随机噪声。 在演示岩石中,我使用一个大的用于景观细节,一个中等的用于岩石中的峭壁,一个小的用于额外的凹凸纹理。
实施基于四叉树的细节系统和纹理流以在相机附近渲染更高清晰度的纹理可以提高质量并减少初始加载时间。
用于渲染摇曳的草地或渲染摄像机附近的苔藓的植被系统也有助于提高场景的保真度,使你可以使用第三人称控制器在地图上四处走动。
原文链接:Three渲染真实景观 — BimAnt