diff --git a/dCommon/NifFile.cpp b/dCommon/NifFile.cpp index 0861fcfbb..5f4585921 100644 --- a/dCommon/NifFile.cpp +++ b/dCommon/NifFile.cpp @@ -687,7 +687,7 @@ namespace NifFile { } } - uint8_t ShaderLookFor(int32_t shader) { + uint16_t ShaderLookFor(int32_t shader) { // By the technique each shader class sets up (ShaderManager's factory table at 0x01889608, indexed by gameValue; // the class's technique setup names it). Checked in the client: 33 and 82 Technique_Basic_NoLighting_VertColor_ // NoTexture, 35 and 84 Technique_Basic_NoLighting_VertColor, 37 Technique_Basic_Lighting_VertColor_NoTexture, @@ -730,6 +730,15 @@ namespace NifFile { case 8: case 10: case 54: case 15: case 23: case 35: case 52: case 57: case 62: case 68: case 70: case 73: case 81: case 84: case 87: case 101: return UNLIT; + // Polished Metal (Technique_Lighting_PolishedMetal_VertColor in Metallic.fx) and Brushed Steel (its noise + // in object space); both load their reflection textures themselves + case 98: + return REFLECTIVE; + case 99: + return REFLECTIVE | BRUSHED; + // LEGO-Emissive + case 53: + return EMISSIVE; default: return 0; } @@ -744,7 +753,7 @@ namespace NifFile { return Parser(data, 0).Dds(block, error); } - std::string Encode(const Model& model, const std::vector& textures, const std::vector& darkTextures) { + std::string Encode(const Model& model, const std::vector& textures, const std::vector& darkTextures, const std::vector& looks) { std::string body; nlohmann::json meshes = nlohmann::json::array(); std::vector names; @@ -771,6 +780,7 @@ namespace NifFile { {"vertexColors", material.vertexColorMode}, {"texture", textureIndex}, {"clampU", material.clampU}, {"clampV", material.clampV}, {"shaderTag", material.shaderTag}, {"darkTexture", uv2 ? darkIndex : -1}, {"uv2", uv2} }; + if (m < looks.size()) entry["look"] = looks[m]; Append(body, mesh.positions.data(), mesh.positions.size() * sizeof(float)); if (!mesh.normals.empty()) { std::vector packed(mesh.normals.size()); diff --git a/dCommon/NifFile.h b/dCommon/NifFile.h index ae3a53ff2..87337e770 100644 --- a/dCommon/NifFile.h +++ b/dCommon/NifFile.h @@ -83,7 +83,7 @@ namespace NifFile { * BasicShaders.fx, LEGOPPLighting.fx and Ocean.fx). Fixed function (-1) has none of these: Gamebryo lights it with * the material and NiVertexColorProperty. */ - enum eShaderLook : uint8_t { + enum eShaderLook : uint16_t { UNLIT = 1, // no lighting: the colors as they are (the "NoLighting" techniques) NO_TEXTURE = 2, // the texture isn't sampled ("NoTexture") NO_VERTEX_COLORS = 4, // vertex colors aren't read @@ -91,11 +91,18 @@ namespace NifFile { // Two textures (base and dark, each with its UV set): blended by the vertex alpha, which is then no opacity // ("Two Layers Blended"), or added, weighted by the material's diffuse red and green ("Two Textures Added") TWO_LAYERS_BLENDED = 16, - TWO_LAYERS_ADDED = 32 + TWO_LAYERS_ADDED = 32, + // Metal (Metallic.fx): the lit color plus an environment map (textures/metal/metal_reflection_*.dds, which the + // shader loads itself) tinted by the vertex color, polished or, with BRUSHED, brushed with object space noise + REFLECTIVE = 64, + BRUSHED = 128, + // LEGO-Emissive: lerp(lit, vertex color, vertex alpha * NiMaterialProperty's emissive red); the vertex alpha is + // that mask, not opacity + EMISSIVE = 256 }; // eShaderLook bits of a shader (mapShaders.gameValue); 0 for the usual lit look and for fixed function - uint8_t ShaderLookFor(int32_t shader); + uint16_t ShaderLookFor(int32_t shader); // Where a node is in the model's space: p' = rotation * p + translation (row-major, scale folded in) struct NodeTransform { @@ -125,9 +132,11 @@ namespace NifFile { * int8 normals (3 per vertex, times 127, padded to 4 bytes) when "normals", float32 UVs (2 per vertex) when "uv", * float32 dark texture UVs when "uv2", uint8 RGBA colors when "colors", then uint16 indices (padded to 4 bytes). * `textures[i]` is where mesh i's texture is (empty: none) and `darkTextures[i]` its dark texture; the header - * lists each once in "textures" and a mesh's "texture" and "darkTexture" index it (-1: none). + * lists each once in "textures" and a mesh's "texture" and "darkTexture" index it (-1: none). `looks[i]`, when + * given, is mesh i's "look" (eShaderLook bits of the shader it is drawn with, for views without a scenery manifest). */ - std::string Encode(const Model& model, const std::vector& textures, const std::vector& darkTextures = {}); + std::string Encode(const Model& model, const std::vector& textures, const std::vector& darkTextures = {}, + const std::vector& looks = {}); /** * A texture stored inside a .nif (NiPixelData or NiPersistentSrcTextureRendererData, block `block`) as a DDS file diff --git a/dDashboardServer/routes/Scenery.cpp b/dDashboardServer/routes/Scenery.cpp index 794ae215f..5308c3f5a 100644 --- a/dDashboardServer/routes/Scenery.cpp +++ b/dDashboardServer/routes/Scenery.cpp @@ -867,6 +867,16 @@ namespace Scenery { return zone->json; } + std::vector MultishaderLooks(const NifFile::Model& model) { + std::vector looks; + for (const auto& mesh : model.meshes) { + std::optional shader; + if (const auto it = g_ShaderValues.find(mesh.material.shaderTag); it != g_ShaderValues.end()) shader = it->second; + looks.push_back(NifFile::ShaderLookFor(NifFile::MultishaderPart(shader))); + } + return looks; + } + bool ZoneReady(uint32_t zoneId) { return g_Zones.Ready(zoneId); } diff --git a/dDashboardServer/routes/Scenery.h b/dDashboardServer/routes/Scenery.h index 909488bc4..5d85c239e 100644 --- a/dDashboardServer/routes/Scenery.h +++ b/dDashboardServer/routes/Scenery.h @@ -3,6 +3,7 @@ #include #include #include +#include struct HTTPReply; struct HTTPContext; @@ -11,6 +12,10 @@ namespace WorldScene { struct Object; } +namespace NifFile { + struct Model; +} + /** * A zone's scenery for the 3D views, drawn from the game client's files the way the client draws it: every object in * the zone's scene files (.lvl) with the model of its render component (RenderComponent.render_asset, a .nif or a @@ -46,6 +51,13 @@ namespace Scenery { // Whether ZoneJson is built (so answering it is quick) bool ZoneReady(uint32_t zoneId); + /** + * The eShaderLook bits (NifFile::ShaderLookFor) of each mesh of a multishader model (a player model: RenderComponent + * shader Multishader), from its parts' tags (mapShaders ids, read by Preload); for NifFile::Encode's `looks`. + * Any thread. + */ + std::vector MultishaderLooks(const NifFile::Model& model); + /** * The zone's flairs (the grass, flowers and small rocks its terrain file strews over it, models from FlairTable) as a * manifest in ZoneJson's form, whose models the same mesh and texture routes serve, plus: distance (how far from the diff --git a/dDashboardServer/routes/UgcRoutes.cpp b/dDashboardServer/routes/UgcRoutes.cpp index c2b30911f..9980d810d 100644 --- a/dDashboardServer/routes/UgcRoutes.cpp +++ b/dDashboardServer/routes/UgcRoutes.cpp @@ -11,6 +11,7 @@ #include "Database.h" #include "UgcIconParams.h" #include "NifFile.h" +#include "Scenery.h" #include "SettingsCatalog.h" #include "SettingsHistory.h" #include "UgcAssemblies.h" @@ -477,7 +478,7 @@ namespace UgcRoutes { }); Route(eHTTPMethod::GET, "/api/ugc/mesh/:id", Perm("properties_view"), - "A player model's generated .nif converted for the 3D view (NifFile::Encode, as the scenery meshes). Query: ?lod=0 (most detailed) " + "A player model's generated .nif converted for the 3D view (NifFile::Encode, as the scenery meshes, with each mesh's shader look). Query: ?lod=0 (most detailed) " "to 3, &version=current|previous, &ao=0 for the mesh before the lighting bake. The header adds triangles and vertices", [](HTTPReply& reply, const HTTPContext& context) { const auto id = PathId(context.path, 3); @@ -495,7 +496,7 @@ namespace UgcRoutes { if (!model) return JsonError(out, eHTTPStatusCode::UNPROCESSABLE_ENTITY, "The .nif can't be read: " + error); out.status = eHTTPStatusCode::OK; out.contentType = eContentType::APPLICATION_OCTET_STREAM; - out.message = NifFile::Encode(*model, std::vector(model->meshes.size())); + out.message = NifFile::Encode(*model, std::vector(model->meshes.size()), {}, Scenery::MultishaderLooks(*model)); out.headers.push_back("Cache-Control: private, no-cache"); }); }); diff --git a/dDashboardServer/static/js/scenery-core.js b/dDashboardServer/static/js/scenery-core.js index 5582c1b15..61efd872f 100644 --- a/dDashboardServer/static/js/scenery-core.js +++ b/dDashboardServer/static/js/scenery-core.js @@ -67,13 +67,16 @@ export function shaderOf(manifest, asset, mesh) { } // NifFile::eShaderLook bits -export const SHADER_LOOK = { UNLIT: 1, NO_TEXTURE: 2, NO_VERTEX_COLORS: 4, MATERIAL_COLOR: 8, TWO_LAYERS_BLENDED: 16, TWO_LAYERS_ADDED: 32 }; +export const SHADER_LOOK = { UNLIT: 1, NO_TEXTURE: 2, NO_VERTEX_COLORS: 4, MATERIAL_COLOR: 8, TWO_LAYERS_BLENDED: 16, TWO_LAYERS_ADDED: 32, REFLECTIVE: 64, BRUSHED: 128, EMISSIVE: 256 }; /** * How a mesh is drawn under the game's shaders, when the manifest has the zone's lighting: {lit, texture, * vertexColors, material, layers} — whether the scene's sun and ambient light it, its texture and vertex colors are * used, whether its NiMaterialProperty colors are (only fixed function and the "Material" shaders use them), and how - * a two layer shader puts its dark texture with the base one ('blended', 'added' or null). Null without + * a two layer shader puts its dark texture with the base one ('blended', 'added' or null), whether it is metal + * ('polished', 'brushed' or null: an environment reflection tinted by the vertex color) and whether it glows + * (LEGO-Emissive: the lit color goes to the vertex color by the vertex alpha times the material's emissive red, so + * the vertex alpha is no opacity). Null without * lighting in the manifest (older servers), for the viewer's own lights. */ export function gameLook(manifest, asset, mesh) { @@ -87,10 +90,18 @@ export function gameLook(manifest, asset, mesh) { // Fixed function reads them as NiVertexColorProperty says; the shaders always do, unless they have none vertexColors: !!(mesh.colors && !(bits & SHADER_LOOK.NO_VERTEX_COLORS) && (!fixedFunction || mesh.vertexColors !== 0)), material: fixedFunction || !!(bits & SHADER_LOOK.MATERIAL_COLOR), - layers: bits & SHADER_LOOK.TWO_LAYERS_BLENDED ? 'blended' : bits & SHADER_LOOK.TWO_LAYERS_ADDED ? 'added' : null + layers: bits & SHADER_LOOK.TWO_LAYERS_BLENDED ? 'blended' : bits & SHADER_LOOK.TWO_LAYERS_ADDED ? 'added' : null, + metal: metalOf(bits), + emissive: !!(bits & SHADER_LOOK.EMISSIVE) }; } +// A shader's metal from its eShaderLook bits: 'polished', 'brushed' or null +export function metalOf(bits) { + if (!(bits & SHADER_LOOK.REFLECTIVE)) return null; + return bits & SHADER_LOOK.BRUSHED ? 'brushed' : 'polished'; +} + /** * Meshes of a model that look the same (texture, colors, blending, sides, attributes) joined into one, so a model * made of many pieces (the zones' "glom" files have over a hundred) costs a few draw calls instead of one per piece. diff --git a/dDashboardServer/static/js/scenery.js b/dDashboardServer/static/js/scenery.js index fb81d93dd..8f265fe3f 100644 --- a/dDashboardServer/static/js/scenery.js +++ b/dDashboardServer/static/js/scenery.js @@ -247,10 +247,18 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress gameNormal = normalize( mat3( modelMatrix ) * gameNormal ); vGameLight = pow( clamp( gameLightColor * max( 0.0, dot( gameNormal, gameLightVec ) ) + gameAmbient, 0.0, 1.0 ), vec3( 2.2 ) );`; + // LEGO-Emissive: the lit color goes to the vertex color by the vertex alpha times the material's emissive red + const EMISSIVE_FRAGMENT = ` +#ifdef USE_COLOR_ALPHA + outgoingLight = mix( outgoingLight, vColor.rgb, clamp( vColor.a * emissiveMix, 0.0, 1.0 ) ); +#endif +#include `; + /** * A material that draws a mesh the way its game shader does (gameLook): unlit by the view's own lights and tone * mapping, the zone's sun and ambient light per vertex when the shader is lit, the material's color only when the - * shader reads it. + * shader reads it. Metal is drawn lit like the rest (the game adds a reflection of its own textures); glowing + * meshes go to their vertex color as the emissive shader does. */ function gameMaterial(options, mesh, alphaMode, look, darkMap = null) { const material = new THREE.MeshBasicMaterial({ @@ -261,7 +269,12 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress const layers = darkMap ? look.layers : null; // TwoLayersAdded_PS: base * material diffuse red + dark * material diffuse green (their animations) const weights = new THREE.Vector2(mesh.diffuse[0], mesh.diffuse[1]); + const emissive = !!look.emissive; material.onBeforeCompile = (shader) => { + if (emissive) { + shader.uniforms.emissiveMix = { value: mesh.emissive[0] }; + shader.fragmentShader = 'uniform float emissiveMix;\n' + shader.fragmentShader.replace('#include ', EMISSIVE_FRAGMENT); + } if (layers) { shader.uniforms.darkMap = { value: darkMap }; shader.uniforms.layerWeights = { value: weights }; @@ -280,7 +293,7 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress shader.fragmentShader = 'varying vec3 vGameLight;\n' + shader.fragmentShader.replace('#include ', '#include \n\treflectedLight.indirectDiffuse *= vGameLight;'); }; - material.customProgramCacheKey = () => 'game:' + (options.map ? alphaMode : '') + ':' + look.lit + ':' + layers; + material.customProgramCacheKey = () => 'game:' + (options.map ? alphaMode : '') + ':' + look.lit + ':' + layers + ':' + emissive; return material; } @@ -292,7 +305,8 @@ export function createScenery({ scene, camera, renderer, urls, focus, onProgress let vertexAlpha = false; // A two layer blend reads the vertex alpha as the mix of its textures, not as opacity const layersBlended = !!(darkMap && look.layers === 'blended'); - if (vertexColors && !layersBlended) for (let i = 3; i < mesh.colors.length && !vertexAlpha; i += 4) vertexAlpha = mesh.colors[i] < 250; + // ... and the emissive shader as how much the vertex color glows + if (vertexColors && !layersBlended && !(look && look.emissive)) for (let i = 3; i < mesh.colors.length && !vertexAlpha; i += 4) vertexAlpha = mesh.colors[i] < 250; const textureAlpha = alphaMode === 'opacity' && !!(map && map.userData.alpha); // The game's shaders take alpha from the vertex colors and texture only; NiMaterialProperty's is for fixed function const materialAlpha = look && !look.material ? 1 : mesh.alpha; diff --git a/dDashboardServer/static/js/ugc-viewer.js b/dDashboardServer/static/js/ugc-viewer.js index 7fbd6cd4f..0a845aea5 100644 --- a/dDashboardServer/static/js/ugc-viewer.js +++ b/dDashboardServer/static/js/ugc-viewer.js @@ -1,11 +1,13 @@ /** * A 3D view of a .nif the UGC server made, from /api/ugc/mesh/:id (the dashboard converts it with NifFile::Encode, - * as it does the scenery's models), with wireframe and vertex color switches and triangle counts. + * as it does the scenery's models), with wireframe and vertex color switches and triangle counts. The metal and glow + * groups (the UGC server's shader settings; each mesh's "look" is its shader's eShaderLook bits) are drawn as metal + * reflecting the view's environment and as unlit glow. */ import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js'; -import { parseModel, mergeMeshes, linearColors } from '/js/scenery-core.js'; +import { parseModel, mergeMeshes, linearColors, metalOf, SHADER_LOOK } from '/js/scenery-core.js'; export function createNifViewer(container) { const renderer = new THREE.WebGLRenderer({ antialias: true }); @@ -116,7 +118,13 @@ export function createNifViewer(container) { // Blending only matters where something is see-through (every shape of a brick model blends) let seeThrough = !!mesh.blend && mesh.alpha < 0.99; if (mesh.blend && hasColors) for (let i = 3; i < mesh.colors.length && !seeThrough; i += 4) seeThrough = mesh.colors[i] < 250; - const material = new THREE.MeshStandardMaterial({ color: baseColor.clone(), vertexColors: hasColors, transparent: seeThrough, roughness: 0.6, metalness: 0 }); + const look = mesh.look || 0; + const metal = metalOf(look); + // Glow: the emissive shader's vertex color, unlit (its vertex alpha is the glow, not opacity) + const material = look & SHADER_LOOK.EMISSIVE + ? new THREE.MeshBasicMaterial({ color: baseColor.clone(), vertexColors: hasColors }) + : new THREE.MeshStandardMaterial({ color: baseColor.clone(), vertexColors: hasColors, transparent: seeThrough, + roughness: metal === 'polished' ? 0.18 : metal === 'brushed' ? 0.45 : 0.6, metalness: metal ? 1 : 0 }); const object = new THREE.Mesh(geometry, material); if (seeThrough) object.renderOrder = 1; root.add(object); diff --git a/tests/dWebTests/NifFileTests.cpp b/tests/dWebTests/NifFileTests.cpp index 300e2550d..84aafa745 100644 --- a/tests/dWebTests/NifFileTests.cpp +++ b/tests/dWebTests/NifFileTests.cpp @@ -492,6 +492,31 @@ TEST(NifFileTests, KnowsWhatEachShaderLeavesOut) { EXPECT_EQ(NifFile::ShaderLookFor(37), NifFile::NO_TEXTURE); // Basic VC NT EXPECT_EQ(NifFile::ShaderLookFor(70), NifFile::UNLIT); // ScrollingUV_NoLight_AnimAlpha EXPECT_EQ(NifFile::ShaderLookFor(32), NifFile::UNLIT | NifFile::NO_VERTEX_COLORS | NifFile::MATERIAL_COLOR); // Basic NL Material + // The UGC server's metal and glow groups: Polished Metal, Brushed Steel, LEGO-Emissive + EXPECT_EQ(NifFile::ShaderLookFor(98), NifFile::REFLECTIVE); + EXPECT_EQ(NifFile::ShaderLookFor(99), NifFile::REFLECTIVE | NifFile::BRUSHED); + EXPECT_EQ(NifFile::ShaderLookFor(53), NifFile::EMISSIVE); + // Through a multishader tag's gameValue (S88 -> 98), as a player model's parts are drawn + EXPECT_EQ(NifFile::ShaderLookFor(NifFile::MultishaderPart(98)), NifFile::REFLECTIVE); + EXPECT_EQ(NifFile::ShaderLookFor(NifFile::MultishaderPart(std::nullopt)), 0); +} + +TEST(NifFileTests, EncodesEachMeshsLook) { + NifFile::Model model; + model.meshes.resize(2); + for (auto& mesh : model.meshes) { + mesh.positions = { 0, 0, 0, 1, 0, 0, 0, 1, 0 }; + mesh.indices = { 0, 1, 2 }; + } + const auto header = [](const std::string& encoded) { + uint32_t length = 0; + std::memcpy(&length, encoded.data(), 4); + return nlohmann::json::parse(encoded.substr(4, length)); + }; + const auto with = header(NifFile::Encode(model, { "", "" }, {}, { NifFile::REFLECTIVE, NifFile::EMISSIVE })); + EXPECT_EQ(with["meshes"][0]["look"], NifFile::REFLECTIVE); + EXPECT_EQ(with["meshes"][1]["look"], NifFile::EMISSIVE); + EXPECT_FALSE(header(NifFile::Encode(model, { "", "" }))["meshes"][0].contains("look")); } // The game client's own meshes, when a client is configured (DLU_CLIENT_RES, else client_location in the build's diff --git a/tests/dWebTests/scenery-core.test.mjs b/tests/dWebTests/scenery-core.test.mjs index 32ca75fc3..e5552511e 100644 --- a/tests/dWebTests/scenery-core.test.mjs +++ b/tests/dWebTests/scenery-core.test.mjs @@ -33,13 +33,13 @@ same(S.textureAlphaMode(manifest, 1, { shaderTag: 1 }), 'decal', 'LEGO part text same(S.textureAlphaMode(manifest, 1, { shaderTag: 30 }), 'opacity', 'Basic VC part texture alpha'); // Lit, textured, vertex colors, no material colors: Basic VC -same(S.gameLook(manifest, 1, { ...colored, shaderTag: 30 }), { lit: true, texture: true, vertexColors: true, material: false, layers: null }, 'Basic VC'); +same(S.gameLook(manifest, 1, { ...colored, shaderTag: 30 }), { lit: true, texture: true, vertexColors: true, material: false, layers: null, metal: null, emissive: false }, 'Basic VC'); // Vertex colors are read even when NiVertexColorProperty ignores them, but only if the mesh has some same(S.gameLook(manifest, 0, { ...colored, vertexColors: 0 }).vertexColors, true, 'shader reads vertex colors'); same(S.gameLook(manifest, 0, {}).vertexColors, false, 'mesh without vertex colors'); -same(S.gameLook(manifest, 3, colored), { lit: false, texture: false, vertexColors: true, material: false, layers: null }, 'Basic NL VC NT'); +same(S.gameLook(manifest, 3, colored), { lit: false, texture: false, vertexColors: true, material: false, layers: null, metal: null, emissive: false }, 'Basic NL VC NT'); // Fixed function: NiVertexColorProperty and the material decide -same(S.gameLook(manifest, 2, { ...colored, vertexColors: 0 }), { lit: true, texture: true, vertexColors: false, material: true, layers: null }, 'fixed function'); +same(S.gameLook(manifest, 2, { ...colored, vertexColors: 0 }), { lit: true, texture: true, vertexColors: false, material: true, layers: null, metal: null, emissive: false }, 'fixed function'); // Without the zone's lighting the viewer lights scenery itself same(S.gameLook({ ...manifest, lighting: null }, 0, colored), null, 'no lighting'); @@ -63,6 +63,15 @@ same(S.gameLook(layered, 0, colored).layers, 'blended', 'two layers blended'); same(S.gameLook(layered, 1, colored).layers, 'added', 'two layers added'); same(S.gameLook(manifest, 0, colored).layers, null, 'one layer'); +// A player model's metal and glow groups (UGC server shader settings): Polished Metal, Brushed Steel, LEGO-Emissive +const shiny = { ...manifest, shaders: [9999], shaderTags: { 1: 5, 88: 98, 89: 99, 46: 53 }, + shaderLooks: { 98: S.SHADER_LOOK.REFLECTIVE, 99: S.SHADER_LOOK.REFLECTIVE | S.SHADER_LOOK.BRUSHED, 53: S.SHADER_LOOK.EMISSIVE } }; +same(S.gameLook(shiny, 0, { ...colored, shaderTag: 88 }).metal, 'polished', 'polished metal'); +same(S.gameLook(shiny, 0, { ...colored, shaderTag: 89 }).metal, 'brushed', 'brushed steel'); +same(S.gameLook(shiny, 0, { ...colored, shaderTag: 46 }).emissive, true, 'emissive'); +same(S.gameLook(shiny, 0, { ...colored, shaderTag: 1 }).metal, null, 'plastic'); +same([S.metalOf(0), S.metalOf(S.SHADER_LOOK.REFLECTIVE), S.metalOf(S.SHADER_LOOK.REFLECTIVE | S.SHADER_LOOK.BRUSHED)], [null, 'polished', 'brushed'], 'metal of look bits'); + // The near plane grows with the distance, within limits same([S.nearPlaneFor(10), S.nearPlaneFor(2000), S.nearPlaneFor(100000)], [0.5, 5, 20], 'near plane');