super-three 0.184.0 → 0.185.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/three.cjs +1260 -927
- package/build/three.core.js +1104 -829
- package/build/three.core.min.js +1 -1
- package/build/three.module.js +156 -98
- package/build/three.module.min.js +1 -1
- package/build/three.tsl.js +11 -4
- package/build/three.tsl.min.js +1 -1
- package/build/three.webgpu.js +7538 -3367
- package/build/three.webgpu.min.js +1 -1
- package/build/three.webgpu.nodes.js +7538 -3366
- package/build/three.webgpu.nodes.min.js +1 -1
- package/examples/jsm/Addons.js +3 -0
- package/examples/jsm/controls/ArcballControls.js +6 -2
- package/examples/jsm/controls/FirstPersonControls.js +79 -42
- package/examples/jsm/controls/TransformControls.js +68 -15
- package/examples/jsm/csm/CSMFrustum.js +31 -6
- package/examples/jsm/csm/CSMShadowNode.js +4 -1
- package/examples/jsm/effects/AsciiEffect.js +14 -1
- package/examples/jsm/exporters/DRACOExporter.js +27 -6
- package/examples/jsm/exporters/GLTFExporter.js +114 -12
- package/examples/jsm/exporters/PLYExporter.js +286 -73
- package/examples/jsm/exporters/USDZExporter.js +347 -112
- package/examples/jsm/generators/CityGenerator.js +346 -0
- package/examples/jsm/generators/ForestGenerator.js +347 -0
- package/examples/jsm/generators/TerrainGenerator.js +504 -0
- package/examples/jsm/generators/TreeGenerator.js +377 -0
- package/examples/jsm/generators/city/SidewalkGenerator.js +253 -0
- package/examples/jsm/generators/city/SkyscraperGenerator.js +1357 -0
- package/examples/jsm/geometries/LoftGeometry.js +355 -0
- package/examples/jsm/helpers/LightProbeGridHelper.js +1 -1
- package/examples/jsm/helpers/LightProbeHelper.js +5 -4
- package/examples/jsm/helpers/ViewHelper.js +16 -6
- package/examples/jsm/inspector/Inspector.js +53 -15
- package/examples/jsm/inspector/RendererInspector.js +152 -36
- package/examples/jsm/inspector/extensions/tsl-graph/TSLGraphEditor.js +1 -1
- package/examples/jsm/inspector/extensions/tsl-graph/TSLGraphLoader.js +1 -1
- package/examples/jsm/inspector/tabs/Console.js +200 -18
- package/examples/jsm/inspector/tabs/Memory.js +8 -0
- package/examples/jsm/inspector/tabs/Parameters.js +64 -4
- package/examples/jsm/inspector/tabs/Performance.js +21 -6
- package/examples/jsm/inspector/tabs/Settings.js +51 -9
- package/examples/jsm/inspector/tabs/Timeline.js +178 -93
- package/examples/jsm/inspector/tabs/Viewer.js +628 -16
- package/examples/jsm/inspector/ui/Graph.js +162 -25
- package/examples/jsm/inspector/ui/Item.js +92 -13
- package/examples/jsm/inspector/ui/List.js +1 -1
- package/examples/jsm/inspector/ui/Profiler.js +90 -45
- package/examples/jsm/inspector/ui/Style.js +1788 -1401
- package/examples/jsm/inspector/ui/Tab.js +5 -3
- package/examples/jsm/inspector/ui/Values.js +71 -6
- package/examples/jsm/inspector/ui/utils.js +130 -10
- package/examples/jsm/libs/basis/README.md +0 -1
- package/examples/jsm/libs/demuxer_mp4.js +3 -3
- package/examples/jsm/libs/draco/README.md +0 -1
- package/examples/jsm/libs/meshopt_clusterizer.module.js +421 -0
- package/examples/jsm/libs/meshopt_simplifier.module.js +627 -0
- package/examples/jsm/libs/mikktspace.module.js +34 -9
- package/examples/jsm/lighting/ClusteredLighting.js +55 -0
- package/examples/jsm/lighting/LightProbeGrid.js +106 -76
- package/examples/jsm/lines/LineMaterial.js +38 -15
- package/examples/jsm/lines/LineSegments2.js +8 -0
- package/examples/jsm/lines/webgpu/LineSegments2.js +8 -0
- package/examples/jsm/loaders/3DMLoader.js +1 -1
- package/examples/jsm/loaders/DRACOLoader.js +46 -13
- package/examples/jsm/loaders/EXRLoader.js +27 -55
- package/examples/jsm/loaders/HDRLoader.js +6 -28
- package/examples/jsm/loaders/KTX2Loader.js +31 -12
- package/examples/jsm/loaders/LDrawLoader.js +8 -8
- package/examples/jsm/loaders/LUT3dlLoader.js +2 -2
- package/examples/jsm/loaders/LUTCubeLoader.js +2 -2
- package/examples/jsm/loaders/LWOLoader.js +4 -0
- package/examples/jsm/loaders/LottieLoader.js +1 -1
- package/examples/jsm/loaders/MaterialXLoader.js +31 -9
- package/examples/jsm/loaders/NRRDLoader.js +3 -3
- package/examples/jsm/loaders/PCDLoader.js +7 -7
- package/examples/jsm/loaders/PLYLoader.js +218 -55
- package/examples/jsm/loaders/SVGLoader.js +547 -495
- package/examples/jsm/loaders/TTFLoader.js +1 -1
- package/examples/jsm/loaders/USDLoader.js +33 -9
- package/examples/jsm/loaders/VRMLLoader.js +1 -1
- package/examples/jsm/loaders/usd/USDAParser.js +93 -20
- package/examples/jsm/loaders/usd/USDCParser.js +1 -1
- package/examples/jsm/loaders/usd/USDComposer.js +52 -19
- package/examples/jsm/misc/TileCreasedNormalsPlugin.js +270 -0
- package/examples/jsm/misc/Volume.js +2 -2
- package/examples/jsm/objects/GroundedSkybox.js +1 -1
- package/examples/jsm/objects/Reflector.js +2 -3
- package/examples/jsm/objects/Water2Mesh.js +1 -1
- package/examples/jsm/physics/AmmoPhysics.js +15 -8
- package/examples/jsm/physics/RapierPhysics.js +27 -2
- package/examples/jsm/shaders/VolumeShader.js +31 -44
- package/examples/jsm/transpiler/GLSLDecoder.js +6 -6
- package/examples/jsm/transpiler/Linker.js +1 -1
- package/examples/jsm/tsl/display/BloomNode.js +59 -19
- package/examples/jsm/tsl/display/DepthOfFieldNode.js +2 -2
- package/examples/jsm/tsl/display/FXAANode.js +11 -12
- package/examples/jsm/tsl/display/GTAONode.js +34 -13
- package/examples/jsm/tsl/display/GodraysNode.js +1 -1
- package/examples/jsm/tsl/display/ImportanceSampledEnvironment.js +560 -0
- package/examples/jsm/tsl/display/PixelationPassNode.js +2 -2
- package/examples/jsm/tsl/display/RecurrentDenoiseNode.js +912 -0
- package/examples/jsm/tsl/display/SSAAPassNode.js +13 -25
- package/examples/jsm/tsl/display/SSGINode.js +89 -39
- package/examples/jsm/tsl/display/SSRNode.js +829 -132
- package/examples/jsm/tsl/display/SSSNode.js +2 -2
- package/examples/jsm/tsl/display/TemporalReprojectNode.js +1023 -0
- package/examples/jsm/tsl/lighting/ClusteredLightsNode.js +622 -0
- package/examples/jsm/tsl/lighting/DynamicLightsNode.js +1 -1
- package/examples/jsm/tsl/math/curlNoise.js +107 -0
- package/examples/jsm/tsl/shadows/TileShadowNode.js +1 -1
- package/examples/jsm/tsl/shadows/TileShadowNodeHelper.js +3 -3
- package/examples/jsm/tsl/utils/GroundedSkybox.js +62 -0
- package/examples/jsm/tsl/utils/RNoise.js +51 -0
- package/examples/jsm/tsl/utils/SpecularHelpers.js +325 -0
- package/examples/jsm/utils/BufferGeometryUtils.js +126 -59
- package/examples/jsm/utils/GeometryCompressionUtils.js +1 -1
- package/examples/jsm/utils/SceneOptimizer.js +1 -1
- package/examples/jsm/webxr/ARButton.js +1 -1
- package/examples/jsm/webxr/WebGLXRFallback.js +89 -0
- package/examples/jsm/webxr/XRControllerModelFactory.js +2 -2
- package/package.json +7 -7
- package/src/Three.TSL.js +10 -3
- package/src/Three.WebGPU.js +4 -0
- package/src/animation/AnimationAction.js +15 -11
- package/src/animation/AnimationClip.js +0 -139
- package/src/animation/KeyframeTrack.js +2 -2
- package/src/animation/PropertyBinding.js +2 -2
- package/src/cameras/Camera.js +2 -2
- package/src/cameras/StereoCamera.js +5 -2
- package/src/constants.js +1 -1
- package/src/core/BufferGeometry.js +29 -7
- package/src/core/Object3D.js +17 -7
- package/src/core/Raycaster.js +1 -1
- package/src/core/RenderTarget.js +15 -2
- package/src/extras/PMREMGenerator.js +6 -4
- package/src/extras/TextureUtils.js +1 -1
- package/src/extras/core/ShapePath.js +149 -160
- package/src/geometries/SphereGeometry.js +8 -3
- package/src/helpers/DirectionalLightHelper.js +2 -0
- package/src/helpers/HemisphereLightHelper.js +2 -0
- package/src/helpers/PointLightHelper.js +2 -0
- package/src/helpers/SpotLightHelper.js +1 -1
- package/src/loaders/DataTextureLoader.js +69 -38
- package/src/loaders/LoadingManager.js +5 -0
- package/src/loaders/MaterialLoader.js +36 -266
- package/src/loaders/ObjectLoader.js +3 -1
- package/src/materials/LineBasicMaterial.js +4 -0
- package/src/materials/Material.js +196 -1
- package/src/materials/MeshBasicMaterial.js +24 -0
- package/src/materials/MeshDepthMaterial.js +10 -0
- package/src/materials/MeshDistanceMaterial.js +10 -0
- package/src/materials/MeshLambertMaterial.js +40 -1
- package/src/materials/MeshMatcapMaterial.js +25 -1
- package/src/materials/MeshNormalMaterial.js +9 -1
- package/src/materials/MeshPhongMaterial.js +40 -1
- package/src/materials/MeshPhysicalMaterial.js +38 -0
- package/src/materials/MeshStandardMaterial.js +42 -1
- package/src/materials/MeshToonMaterial.js +34 -1
- package/src/materials/PointsMaterial.js +7 -0
- package/src/materials/ShaderMaterial.js +86 -0
- package/src/materials/SpriteMaterial.js +7 -0
- package/src/materials/nodes/Line2NodeMaterial.js +378 -322
- package/src/materials/nodes/MeshBasicNodeMaterial.js +2 -2
- package/src/materials/nodes/MeshNormalNodeMaterial.js +2 -2
- package/src/materials/nodes/MeshPhongNodeMaterial.js +0 -9
- package/src/materials/nodes/MeshPhysicalNodeMaterial.js +0 -30
- package/src/materials/nodes/MeshSSSNodeMaterial.js +0 -13
- package/src/materials/nodes/MeshStandardNodeMaterial.js +0 -11
- package/src/materials/nodes/NodeMaterial.js +115 -71
- package/src/materials/nodes/SpriteNodeMaterial.js +0 -10
- package/src/materials/nodes/manager/NodeMaterialObserver.js +49 -20
- package/src/math/Box3.js +5 -0
- package/src/math/FrustumArray.js +103 -133
- package/src/math/Interpolant.js +1 -1
- package/src/math/MathUtils.js +2 -2
- package/src/math/Matrix3.js +11 -0
- package/src/math/Matrix4.js +28 -3
- package/src/math/Vector2.js +2 -2
- package/src/math/Vector3.js +2 -2
- package/src/math/Vector4.js +2 -2
- package/src/math/interpolants/BezierInterpolant.js +4 -6
- package/src/nodes/Nodes.js +2 -5
- package/src/nodes/TSL.js +8 -6
- package/src/nodes/accessors/Arrays.js +4 -4
- package/src/nodes/accessors/Batch.js +108 -0
- package/src/nodes/accessors/Bitangent.js +2 -2
- package/src/nodes/accessors/BufferAttributeNode.js +13 -2
- package/src/nodes/accessors/Camera.js +7 -7
- package/src/nodes/accessors/ClippingNode.js +1 -1
- package/src/nodes/accessors/Instance.js +264 -0
- package/src/nodes/accessors/MaterialNode.js +18 -2
- package/src/nodes/accessors/MaterialProperties.js +4 -3
- package/src/nodes/accessors/{MorphNode.js → Morph.js} +99 -112
- package/src/nodes/accessors/Normal.js +13 -13
- package/src/nodes/accessors/Position.js +5 -1
- package/src/nodes/accessors/ReferenceNode.js +1 -0
- package/src/nodes/accessors/ReflectVector.js +3 -3
- package/src/nodes/accessors/SceneProperties.js +1 -1
- package/src/nodes/accessors/Skinning.js +263 -0
- package/src/nodes/accessors/StorageBufferNode.js +2 -2
- package/src/nodes/accessors/StorageTexture3DNode.js +100 -0
- package/src/nodes/accessors/StorageTextureNode.js +39 -7
- package/src/nodes/accessors/Tangent.js +4 -4
- package/src/nodes/accessors/TextureNode.js +60 -5
- package/src/nodes/code/FunctionNode.js +2 -16
- package/src/nodes/core/MRTNode.js +7 -2
- package/src/nodes/core/Node.js +39 -2
- package/src/nodes/core/NodeBuilder.js +201 -41
- package/src/nodes/core/NodeUtils.js +13 -13
- package/src/nodes/core/OutputStructNode.js +1 -1
- package/src/nodes/core/OverrideContextNode.js +153 -0
- package/src/nodes/core/PropertyNode.js +51 -5
- package/src/nodes/core/StackNode.js +56 -34
- package/src/nodes/core/StructNode.js +4 -6
- package/src/nodes/core/StructTypeNode.js +6 -6
- package/src/nodes/core/VarNode.js +8 -0
- package/src/nodes/core/VaryingNode.js +12 -2
- package/src/nodes/display/BlendModes.js +1 -41
- package/src/nodes/display/BumpMapNode.js +6 -1
- package/src/nodes/display/ColorAdjustment.js +10 -9
- package/src/nodes/display/FrontFacingNode.js +38 -9
- package/src/nodes/display/NormalMapNode.js +2 -2
- package/src/nodes/display/PassNode.js +35 -43
- package/src/nodes/display/PremultiplyAlphaFunctions.js +39 -0
- package/src/nodes/display/RenderOutputNode.js +12 -3
- package/src/nodes/functions/VolumetricLightingModel.js +40 -7
- package/src/nodes/gpgpu/WorkgroupInfoNode.js +1 -1
- package/src/nodes/lighting/EnvironmentNode.js +2 -2
- package/src/nodes/lighting/IESSpotLightNode.js +40 -2
- package/src/nodes/lighting/LightingContextNode.js +10 -2
- package/src/nodes/lighting/LightsNode.js +90 -37
- package/src/nodes/lighting/PointShadowNode.js +6 -1
- package/src/nodes/lighting/ShadowFilterNode.js +21 -52
- package/src/nodes/lighting/ShadowNode.js +34 -14
- package/src/nodes/materialx/lib/mx_noise.js +2 -2
- package/src/nodes/math/ConditionalNode.js +3 -3
- package/src/nodes/math/MathNode.js +39 -11
- package/src/nodes/math/OperatorNode.js +23 -23
- package/src/nodes/parsers/GLSLNodeFunction.js +1 -1
- package/src/nodes/pmrem/PMREMNode.js +33 -2
- package/src/nodes/tsl/TSLCore.js +63 -18
- package/src/nodes/utils/EquirectUV.js +30 -5
- package/src/nodes/utils/FunctionOverloadingNode.js +1 -1
- package/src/nodes/utils/Packing.js +35 -4
- package/src/nodes/utils/RTTNode.js +40 -24
- package/src/nodes/utils/ReflectorNode.js +1 -0
- package/src/nodes/utils/StorageArrayElementNode.js +1 -1
- package/src/objects/BatchedMesh.js +22 -14
- package/src/objects/InstancedMesh.js +0 -11
- package/src/objects/Skeleton.js +0 -9
- package/src/renderers/WebGLRenderer.js +19 -15
- package/src/renderers/common/Backend.js +49 -5
- package/src/renderers/common/Bindings.js +114 -45
- package/src/renderers/common/Buffer.js +9 -0
- package/src/renderers/common/ClippingContext.js +10 -2
- package/src/renderers/common/Info.js +63 -22
- package/src/renderers/common/Lighting.js +53 -5
- package/src/renderers/common/RenderList.js +41 -4
- package/src/renderers/common/RenderObject.js +73 -13
- package/src/renderers/common/Renderer.js +281 -108
- package/src/renderers/common/Sampler.js +16 -48
- package/src/renderers/common/Textures.js +32 -5
- package/src/renderers/common/TimestampQueryPool.js +1 -1
- package/src/renderers/common/UniformsGroup.js +31 -9
- package/src/renderers/common/XRManager.js +340 -22
- package/src/renderers/common/extras/PMREMGenerator.js +15 -38
- package/src/renderers/common/nodes/NodeBuilderState.js +9 -1
- package/src/renderers/common/nodes/NodeLibrary.js +4 -4
- package/src/renderers/common/nodes/NodeManager.js +63 -14
- package/src/renderers/common/nodes/NodeUniformBuffer.js +13 -0
- package/src/renderers/shaders/ShaderChunk/common.glsl.js +13 -4
- package/src/renderers/shaders/ShaderChunk/defaultnormal_vertex.glsl.js +0 -6
- package/src/renderers/shaders/ShaderChunk/envmap_fragment.glsl.js +1 -1
- package/src/renderers/shaders/ShaderChunk/envmap_physical_pars_fragment.glsl.js +2 -2
- package/src/renderers/shaders/ShaderChunk/envmap_vertex.glsl.js +1 -1
- package/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js +1 -1
- package/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js +1 -1
- package/src/renderers/shaders/ShaderChunk/normal_fragment_begin.glsl.js +2 -2
- package/src/renderers/shaders/ShaderChunk/normal_vertex.glsl.js +6 -0
- package/src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js +1 -1
- package/src/renderers/shaders/ShaderChunk/transmission_fragment.glsl.js +1 -1
- package/src/renderers/shaders/ShaderLib/distance.glsl.js +1 -1
- package/src/renderers/webgl/WebGLOutput.js +2 -1
- package/src/renderers/webgl/WebGLProgram.js +5 -4
- package/src/renderers/webgl/WebGLPrograms.js +12 -5
- package/src/renderers/webgl/WebGLRenderLists.js +9 -1
- package/src/renderers/webgl/WebGLShaderCache.js +5 -11
- package/src/renderers/webgl/WebGLTextures.js +20 -7
- package/src/renderers/webgl/WebGLUniformsGroups.js +66 -38
- package/src/renderers/webgl-fallback/WebGLBackend.js +84 -25
- package/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +138 -8
- package/src/renderers/webgl-fallback/utils/WebGLAttributeUtils.js +1 -1
- package/src/renderers/webgl-fallback/utils/WebGLTextureUtils.js +52 -22
- package/src/renderers/webgl-fallback/utils/WebGLUtils.js +4 -2
- package/src/renderers/webgpu/WebGPUBackend.js +480 -169
- package/src/renderers/webgpu/descriptors/GPUBindGroupDescriptor.js +48 -0
- package/src/renderers/webgpu/descriptors/GPUBufferDescriptor.js +57 -0
- package/src/renderers/webgpu/descriptors/GPUCommandEncoderDescriptor.js +30 -0
- package/src/renderers/webgpu/descriptors/GPUComputePassDescriptor.js +38 -0
- package/src/renderers/webgpu/descriptors/GPUComputePipelineDescriptor.js +48 -0
- package/src/renderers/webgpu/descriptors/GPUCopyExternalImageDestInfo.js +47 -0
- package/src/renderers/webgpu/descriptors/GPUCopyExternalImageSourceInfo.js +50 -0
- package/src/renderers/webgpu/descriptors/GPUExtent3D.js +51 -0
- package/src/renderers/webgpu/descriptors/GPUPipelineLayoutDescriptor.js +39 -0
- package/src/renderers/webgpu/descriptors/GPUQuerySetDescriptor.js +47 -0
- package/src/renderers/webgpu/descriptors/GPURenderBundleEncoderDescriptor.js +74 -0
- package/src/renderers/webgpu/descriptors/GPURenderPassColorAttachment.js +72 -0
- package/src/renderers/webgpu/descriptors/GPURenderPassDepthStencilAttachment.js +99 -0
- package/src/renderers/webgpu/descriptors/GPURenderPassDescriptor.js +72 -0
- package/src/renderers/webgpu/descriptors/GPURenderPassTimestampWrites.js +49 -0
- package/src/renderers/webgpu/descriptors/GPURenderPipelineDescriptor.js +130 -0
- package/src/renderers/webgpu/descriptors/GPUSamplerDescriptor.js +119 -0
- package/src/renderers/webgpu/descriptors/GPUShaderModuleDescriptor.js +46 -0
- package/src/renderers/webgpu/descriptors/GPUTexelCopyBufferInfo.js +57 -0
- package/src/renderers/webgpu/descriptors/GPUTexelCopyBufferLayout.js +48 -0
- package/src/renderers/webgpu/descriptors/GPUTexelCopyTextureInfo.js +61 -0
- package/src/renderers/webgpu/descriptors/GPUTextureDescriptor.js +99 -0
- package/src/renderers/webgpu/descriptors/GPUTextureViewDescriptor.js +108 -0
- package/src/renderers/webgpu/nodes/StandardNodeLibrary.js +0 -1
- package/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +164 -7
- package/src/renderers/webgpu/nodes/WGSLNodeFunction.js +1 -1
- package/src/renderers/webgpu/utils/WebGPUAttributeUtils.js +98 -38
- package/src/renderers/webgpu/utils/WebGPUBindingUtils.js +72 -68
- package/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +164 -69
- package/src/renderers/webgpu/utils/WebGPUTexturePassUtils.js +158 -95
- package/src/renderers/webgpu/utils/WebGPUTextureUtils.js +297 -141
- package/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js +33 -18
- package/src/renderers/webgpu/utils/WebGPUUtils.js +38 -3
- package/src/textures/DepthTexture.js +1 -1
- package/src/textures/ExternalTexture.js +0 -3
- package/examples/fonts/LICENSE +0 -13
- package/examples/fonts/MPLUSRounded1c/MPLUSRounded1c-Regular.typeface.json.zip +0 -0
- package/examples/fonts/MPLUSRounded1c/OFL.txt +0 -91
- package/examples/fonts/README.md +0 -11
- package/examples/fonts/droid/NOTICE +0 -190
- package/examples/fonts/droid/README.txt +0 -18
- package/examples/fonts/droid/droid_sans_bold.typeface.json +0 -1
- package/examples/fonts/droid/droid_sans_mono_regular.typeface.json +0 -1
- package/examples/fonts/droid/droid_sans_regular.typeface.json +0 -1
- package/examples/fonts/droid/droid_serif_bold.typeface.json +0 -1
- package/examples/fonts/droid/droid_serif_regular.typeface.json +0 -1
- package/examples/fonts/gentilis_bold.typeface.json +0 -1
- package/examples/fonts/gentilis_regular.typeface.json +0 -1
- package/examples/fonts/helvetiker_bold.typeface.json +0 -1
- package/examples/fonts/helvetiker_regular.typeface.json +0 -1
- package/examples/fonts/optimer_bold.typeface.json +0 -1
- package/examples/fonts/optimer_regular.typeface.json +0 -1
- package/examples/fonts/ttf/README.md +0 -9
- package/examples/fonts/ttf/kenpixel.ttf +0 -0
- package/examples/jsm/inspector/extensions/extensions.json +0 -6
- package/examples/jsm/libs/ammo.wasm.js +0 -822
- package/examples/jsm/libs/ammo.wasm.wasm +0 -0
- package/examples/jsm/libs/draco/draco_encoder.js +0 -33
- package/examples/jsm/libs/draco/gltf/draco_encoder.js +0 -33
- package/examples/jsm/libs/lottie_canvas.module.js +0 -14849
- package/examples/jsm/libs/opentype.module.js +0 -14506
- package/examples/jsm/libs/rhino3dm/rhino3dm.js +0 -21
- package/examples/jsm/libs/rhino3dm/rhino3dm.module.js +0 -16
- package/examples/jsm/libs/rhino3dm/rhino3dm.wasm +0 -0
- package/examples/jsm/lighting/TiledLighting.js +0 -42
- package/examples/jsm/tsl/display/AnamorphicNode.js +0 -293
- package/examples/jsm/tsl/lighting/TiledLightsNode.js +0 -442
- package/src/nodes/accessors/BatchNode.js +0 -163
- package/src/nodes/accessors/InstanceNode.js +0 -349
- package/src/nodes/accessors/InstancedMeshNode.js +0 -50
- package/src/nodes/accessors/SkinningNode.js +0 -328
|
@@ -0,0 +1,1357 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoxGeometry,
|
|
3
|
+
BufferAttribute,
|
|
4
|
+
BufferGeometry,
|
|
5
|
+
ExtrudeGeometry,
|
|
6
|
+
InterpolationSamplingMode,
|
|
7
|
+
InterpolationSamplingType,
|
|
8
|
+
LatheGeometry,
|
|
9
|
+
Matrix3,
|
|
10
|
+
Matrix4,
|
|
11
|
+
Mesh,
|
|
12
|
+
MeshStandardMaterial,
|
|
13
|
+
Path,
|
|
14
|
+
PlaneGeometry,
|
|
15
|
+
ShapeGeometry,
|
|
16
|
+
Shape,
|
|
17
|
+
Sphere,
|
|
18
|
+
Vector2,
|
|
19
|
+
Vector3
|
|
20
|
+
} from 'three';
|
|
21
|
+
|
|
22
|
+
import { MeshStandardNodeMaterial } from 'three/webgpu';
|
|
23
|
+
import { attribute, cameraPosition, color, cross, dot, float, floor, Fn, fract, fwidth, hash as ihash, mix, mod, modelWorldMatrixInverse, mx_fractal_noise_float, mx_noise_float, normalLocal, normalView, normalWorldGeometry, positionLocal, positionView, positionWorld, select, smoothstep, step, uint, uv, varying, vec2, vec3, vec4 } from 'three/tsl';
|
|
24
|
+
|
|
25
|
+
import { mergeGeometries } from '../../utils/BufferGeometryUtils.js';
|
|
26
|
+
|
|
27
|
+
const _scale = /*@__PURE__*/ new Vector3();
|
|
28
|
+
const _point = /*@__PURE__*/ new Vector3();
|
|
29
|
+
const _normalMatrix = /*@__PURE__*/ new Matrix3();
|
|
30
|
+
const _identity = /*@__PURE__*/ new Matrix4();
|
|
31
|
+
|
|
32
|
+
// material-zone codes baked per vertex into the merged geometry, so one material can
|
|
33
|
+
// branch on partId and shade every zone
|
|
34
|
+
const PartId = { WALL: 0, PIER: 1, FRAME: 2, ORNAMENT: 3, GLASS: 4, AC: 5 };
|
|
35
|
+
const { WALL, PIER, FRAME, ORNAMENT, GLASS, AC } = PartId;
|
|
36
|
+
|
|
37
|
+
// fraction of a floor's height taken by the glazed opening; the remainder is
|
|
38
|
+
// the spandrel band. shared by the window module and the spandrels so they tile.
|
|
39
|
+
const WINDOW_HEIGHT_RATIO = 0.62;
|
|
40
|
+
|
|
41
|
+
// width of the flat window-frame band around the glazing; shared by the frame module
|
|
42
|
+
// and the glass pane so the pane always tucks inside the frame
|
|
43
|
+
const WINDOW_BORDER = 0.1;
|
|
44
|
+
|
|
45
|
+
// the masonry course module ( brick height × length ). the generator snaps floor and
|
|
46
|
+
// bay dimensions to it, and the material's coursing reads the same values, so the
|
|
47
|
+
// procedural brickwork lines up with the geometry
|
|
48
|
+
const BRICK = { height: 0.3, length: 0.6 };
|
|
49
|
+
|
|
50
|
+
// merging requires all-indexed or all-non-indexed inputs; extrusions are
|
|
51
|
+
// non-indexed while boxes/planes are indexed, so normalize before merging
|
|
52
|
+
|
|
53
|
+
function merge( geometries ) {
|
|
54
|
+
|
|
55
|
+
return mergeGeometries( geometries.map( ( g ) => g.index ? g.toNonIndexed() : g ) );
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function nonIndexed( geometry ) {
|
|
60
|
+
|
|
61
|
+
return geometry.index ? geometry.toNonIndexed() : geometry;
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// the unit box is identical for every building's shell boxes — build it once
|
|
66
|
+
const _unitBox = /*@__PURE__*/ nonIndexed( new BoxGeometry( 1, 1, 1 ) );
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Bakes a list of instance groups into one non-indexed BufferGeometry. Each group is a
|
|
70
|
+
* base geometry ( position + normal + uv ), an array of Matrix4 placements and a `partId`
|
|
71
|
+
* written to a per-vertex attribute. Transforming straight into preallocated typed arrays
|
|
72
|
+
* avoids mergeGeometries' per-instance allocations; the result is one geometry, ready for
|
|
73
|
+
* a single draw call and the compute rasterizer.
|
|
74
|
+
*/
|
|
75
|
+
function bakeGroups( groups ) {
|
|
76
|
+
|
|
77
|
+
let total = 0;
|
|
78
|
+
for ( const group of groups ) total += group.geometry.attributes.position.count * group.matrices.length;
|
|
79
|
+
|
|
80
|
+
const position = new Float32Array( total * 3 );
|
|
81
|
+
const normal = new Float32Array( total * 3 );
|
|
82
|
+
const uv = new Float32Array( total * 2 );
|
|
83
|
+
const partId = new Float32Array( total );
|
|
84
|
+
// per-window interior-mapping room ( centre + size ) the glass pane looks into; only
|
|
85
|
+
// the glass group writes it, every other vertex stays zero. baked per vertex so the
|
|
86
|
+
// material reads each building's own room sizes without a global uniform.
|
|
87
|
+
const roomCenter = new Float32Array( total * 3 );
|
|
88
|
+
const roomSize = new Float32Array( total * 2 );
|
|
89
|
+
|
|
90
|
+
let w = 0;
|
|
91
|
+
|
|
92
|
+
// the bounding sphere falls out of the AABB gathered while transforming, sparing a
|
|
93
|
+
// second full pass over the positions ( computeBoundingSphere )
|
|
94
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
95
|
+
let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
|
|
96
|
+
|
|
97
|
+
for ( const group of groups ) {
|
|
98
|
+
|
|
99
|
+
const geometry = group.geometry;
|
|
100
|
+
const P = geometry.attributes.position.array;
|
|
101
|
+
const N = geometry.attributes.normal.array;
|
|
102
|
+
const U = geometry.attributes.uv.array;
|
|
103
|
+
const count = geometry.attributes.position.count;
|
|
104
|
+
const id = group.partId;
|
|
105
|
+
const rooms = group.rooms; // per-instance { center, size }, glass only
|
|
106
|
+
const rigid = group.rigid === true; // pure rotation ( + translation ): the normal matrix is the rotation itself
|
|
107
|
+
|
|
108
|
+
for ( let i = 0; i < group.matrices.length; i ++ ) {
|
|
109
|
+
|
|
110
|
+
const room = rooms ? rooms[ i ] : null;
|
|
111
|
+
|
|
112
|
+
const matrix = group.matrices[ i ];
|
|
113
|
+
const e = matrix.elements;
|
|
114
|
+
const e0 = e[ 0 ], e1 = e[ 1 ], e2 = e[ 2 ], e4 = e[ 4 ], e5 = e[ 5 ], e6 = e[ 6 ], e8 = e[ 8 ], e9 = e[ 9 ], e10 = e[ 10 ], e12 = e[ 12 ], e13 = e[ 13 ], e14 = e[ 14 ];
|
|
115
|
+
|
|
116
|
+
// for a rigid frame the inverse-transpose equals the rotation, so its columns
|
|
117
|
+
// are read straight from the matrix and the per-instance 3×3 inverse is skipped
|
|
118
|
+
let n0, n1, n2, n3, n4, n5, n6, n7, n8;
|
|
119
|
+
|
|
120
|
+
if ( rigid ) {
|
|
121
|
+
|
|
122
|
+
n0 = e0; n1 = e1; n2 = e2; n3 = e4; n4 = e5; n5 = e6; n6 = e8; n7 = e9; n8 = e10;
|
|
123
|
+
|
|
124
|
+
} else {
|
|
125
|
+
|
|
126
|
+
const ne = _normalMatrix.getNormalMatrix( matrix ).elements;
|
|
127
|
+
n0 = ne[ 0 ]; n1 = ne[ 1 ]; n2 = ne[ 2 ]; n3 = ne[ 3 ]; n4 = ne[ 4 ]; n5 = ne[ 5 ]; n6 = ne[ 6 ]; n7 = ne[ 7 ]; n8 = ne[ 8 ];
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for ( let v = 0; v < count; v ++ ) {
|
|
132
|
+
|
|
133
|
+
const v3 = v * 3, w3 = w * 3;
|
|
134
|
+
const x = P[ v3 ], y = P[ v3 + 1 ], z = P[ v3 + 2 ];
|
|
135
|
+
const wx = e0 * x + e4 * y + e8 * z + e12;
|
|
136
|
+
const wy = e1 * x + e5 * y + e9 * z + e13;
|
|
137
|
+
const wz = e2 * x + e6 * y + e10 * z + e14;
|
|
138
|
+
position[ w3 ] = wx; position[ w3 + 1 ] = wy; position[ w3 + 2 ] = wz;
|
|
139
|
+
if ( wx < minX ) minX = wx; if ( wx > maxX ) maxX = wx;
|
|
140
|
+
if ( wy < minY ) minY = wy; if ( wy > maxY ) maxY = wy;
|
|
141
|
+
if ( wz < minZ ) minZ = wz; if ( wz > maxZ ) maxZ = wz;
|
|
142
|
+
|
|
143
|
+
const nx = N[ v3 ], ny = N[ v3 + 1 ], nz = N[ v3 + 2 ];
|
|
144
|
+
const tx = n0 * nx + n3 * ny + n6 * nz, ty = n1 * nx + n4 * ny + n7 * nz, tz = n2 * nx + n5 * ny + n8 * nz;
|
|
145
|
+
const inv = 1 / ( Math.sqrt( tx * tx + ty * ty + tz * tz ) || 1 );
|
|
146
|
+
normal[ w3 ] = tx * inv; normal[ w3 + 1 ] = ty * inv; normal[ w3 + 2 ] = tz * inv;
|
|
147
|
+
|
|
148
|
+
uv[ w * 2 ] = U[ v * 2 ]; uv[ w * 2 + 1 ] = U[ v * 2 + 1 ];
|
|
149
|
+
partId[ w ] = id;
|
|
150
|
+
|
|
151
|
+
if ( room !== null ) {
|
|
152
|
+
|
|
153
|
+
roomCenter[ w3 ] = room.center.x; roomCenter[ w3 + 1 ] = room.center.y; roomCenter[ w3 + 2 ] = room.center.z;
|
|
154
|
+
roomSize[ w * 2 ] = room.size.x; roomSize[ w * 2 + 1 ] = room.size.y;
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
w ++;
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const geometry = new BufferGeometry();
|
|
167
|
+
geometry.setAttribute( 'position', new BufferAttribute( position, 3 ) );
|
|
168
|
+
geometry.setAttribute( 'normal', new BufferAttribute( normal, 3 ) );
|
|
169
|
+
geometry.setAttribute( 'uv', new BufferAttribute( uv, 2 ) );
|
|
170
|
+
geometry.setAttribute( 'partId', new BufferAttribute( partId, 1 ) );
|
|
171
|
+
geometry.setAttribute( 'roomCenter', new BufferAttribute( roomCenter, 3 ) );
|
|
172
|
+
geometry.setAttribute( 'roomSize', new BufferAttribute( roomSize, 2 ) );
|
|
173
|
+
|
|
174
|
+
geometry.boundingSphere = new Sphere(
|
|
175
|
+
new Vector3( ( minX + maxX ) / 2, ( minY + maxY ) / 2, ( minZ + maxZ ) / 2 ),
|
|
176
|
+
Math.hypot( maxX - minX, maxY - minY, maxZ - minZ ) / 2
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return geometry;
|
|
180
|
+
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// deterministic PRNG (mulberry32) so a given seed always yields the same tower
|
|
184
|
+
|
|
185
|
+
function createRandom( seed ) {
|
|
186
|
+
|
|
187
|
+
let s = ( seed >>> 0 ) || 1;
|
|
188
|
+
|
|
189
|
+
return function () {
|
|
190
|
+
|
|
191
|
+
s = ( s + 0x6D2B79F5 ) | 0;
|
|
192
|
+
let t = Math.imul( s ^ ( s >>> 15 ), 1 | s );
|
|
193
|
+
t = ( t + Math.imul( t ^ ( t >>> 7 ), 61 | t ) ) ^ t;
|
|
194
|
+
return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296;
|
|
195
|
+
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// a stable per-floor hash ( from the floor index and the face origin ) used to pick the
|
|
201
|
+
// interior-mapping room module per floor without allocating a closure each floor
|
|
202
|
+
function floorHash( f, frame, k ) {
|
|
203
|
+
|
|
204
|
+
const s = Math.sin( f * 12.9898 + frame.origin.x * 0.07 + frame.origin.z * 0.131 + k ) * 43758.5453;
|
|
205
|
+
return s - Math.floor( s );
|
|
206
|
+
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// the seed-driven "style" of a tower: footprint proportions, tier split and the
|
|
210
|
+
// shaping of piers and base arches. these sit between the fixed defaults and the
|
|
211
|
+
// caller's parameters, so any parameter passed in still overrides its seeded value.
|
|
212
|
+
|
|
213
|
+
function randomStyle( random ) {
|
|
214
|
+
|
|
215
|
+
const base = 0.10 + random() * 0.07;
|
|
216
|
+
const crown = 0.08 + random() * 0.08;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
footprint: { width: 26 + random() * 18, depth: 20 + random() * 14 },
|
|
220
|
+
tierFractions: { base, crown },
|
|
221
|
+
pierWidth: 0.4 + random() * 0.4,
|
|
222
|
+
pierDepth: 0.3 + random() * 0.3,
|
|
223
|
+
windowReveal: 0.12 + random() * 0.1,
|
|
224
|
+
stringCourseHeight: 0.5 + random() * 0.5,
|
|
225
|
+
archBayWidthRatio: Math.round( 1.5 + random() * 1.5 ),
|
|
226
|
+
archRise: 0.4 + random() * 0.5
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Generates intricate, tripartite "Beaux-Arts / Neo-Gothic" terracotta
|
|
233
|
+
* skyscrapers from a small set of parameters.
|
|
234
|
+
*
|
|
235
|
+
* The mass is read as a footprint polygon (a rectangle with one chamfered
|
|
236
|
+
* corner) split into vertical faces, each split into three tiers — a tall
|
|
237
|
+
* arcaded base, a repeating shaft and an ornate crown — then into floors and
|
|
238
|
+
* bays. A handful of authored pieces (a pier, a window, a cornice profile, a
|
|
239
|
+
* gothic arch) are instanced across the whole tower, then baked — together with
|
|
240
|
+
* the bespoke base arcade — into a single non-indexed BufferGeometry tagged with
|
|
241
|
+
* a per-vertex `partId` ({@link PartId}) so one material can shade every zone.
|
|
242
|
+
*
|
|
243
|
+
* The generator is material agnostic — it only produces geometry. Pass a single
|
|
244
|
+
* material (e.g. a TSL node material that branches on `partId`) to dress it.
|
|
245
|
+
*
|
|
246
|
+
* ```js
|
|
247
|
+
* const generator = new SkyscraperGenerator( { seed: 35, totalHeight: 140 }, material );
|
|
248
|
+
* scene.add( generator.build() ); // a single Mesh
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
class SkyscraperGenerator {
|
|
252
|
+
|
|
253
|
+
constructor( parameters = {}, material = null ) {
|
|
254
|
+
|
|
255
|
+
this.parameters = parameters; // caller overrides; defaults + seed fill the rest at build time
|
|
256
|
+
this.material = material; // a single material; the look is driven by the baked `partId` attribute
|
|
257
|
+
|
|
258
|
+
this.mesh = null;
|
|
259
|
+
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
setParameters( parameters ) {
|
|
263
|
+
|
|
264
|
+
Object.assign( this.parameters, parameters );
|
|
265
|
+
|
|
266
|
+
return this;
|
|
267
|
+
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
build() {
|
|
271
|
+
|
|
272
|
+
const random = createRandom( this.parameters.seed ?? SkyscraperGenerator.defaults.seed );
|
|
273
|
+
|
|
274
|
+
// precedence: fixed defaults < seed-driven style < caller parameters
|
|
275
|
+
|
|
276
|
+
const p = Object.assign( {}, SkyscraperGenerator.defaults, randomStyle( random ), this.parameters );
|
|
277
|
+
|
|
278
|
+
// snap the masonry-driving dimensions to the brick module so the procedural
|
|
279
|
+
// brickwork ( courses up local Y, columns along each face ) lines up with the
|
|
280
|
+
// geometry: a whole number of courses per floor and bricks per bay
|
|
281
|
+
const vModule = BRICK.height * 2; // a course pair, so floor / window halves still land on a joint
|
|
282
|
+
p.floorHeight = Math.max( vModule * 3, Math.round( p.floorHeight / vModule ) * vModule );
|
|
283
|
+
p.windowHeight = Math.round( p.floorHeight * WINDOW_HEIGHT_RATIO / vModule ) * vModule;
|
|
284
|
+
p.bayWidth = Math.max( BRICK.length * 3, Math.round( p.bayWidth / BRICK.length ) * BRICK.length );
|
|
285
|
+
p.pierWidth = Math.max( BRICK.length, Math.round( p.pierWidth / BRICK.length ) * BRICK.length );
|
|
286
|
+
|
|
287
|
+
// vertical layout: base / shaft / crown as whole floor counts, so every floor
|
|
288
|
+
// line sits on a course ( the requested total height is rounded to suit )
|
|
289
|
+
const floors = Math.max( 3, Math.round( p.totalHeight / p.floorHeight ) );
|
|
290
|
+
const baseFloors = Math.max( 1, Math.round( floors * p.tierFractions.base ) );
|
|
291
|
+
const crownFloors = Math.max( 1, Math.round( floors * p.tierFractions.crown ) );
|
|
292
|
+
const shaftFloors = Math.max( 1, floors - baseFloors - crownFloors );
|
|
293
|
+
|
|
294
|
+
const baseHeight = baseFloors * p.floorHeight;
|
|
295
|
+
const crownHeight = crownFloors * p.floorHeight;
|
|
296
|
+
const shaftHeight = shaftFloors * p.floorHeight;
|
|
297
|
+
p.totalHeight = baseHeight + shaftHeight + crownHeight;
|
|
298
|
+
|
|
299
|
+
const baseTop = baseHeight;
|
|
300
|
+
const shaftTop = baseHeight + shaftHeight;
|
|
301
|
+
|
|
302
|
+
// one accumulator per kind of part, mostly instance matrices. kept separate so the
|
|
303
|
+
// bake below can order them by draw order ( which controls overdraw ), not build order.
|
|
304
|
+
|
|
305
|
+
const windows = [];
|
|
306
|
+
const glass = [];
|
|
307
|
+
const glassRooms = []; // per-glass interior-mapping room ( centre + size ), aligned with `glass`
|
|
308
|
+
const backWalls = []; // the thin wall closing the volume behind the glass
|
|
309
|
+
const bands = []; // spandrel bands, one at each floor line
|
|
310
|
+
const piers = new Map(); // pier height -> matrices, so each tier's continuous piers share one geometry
|
|
311
|
+
const trim = []; // cornices and parapets ( axis-aligned unit boxes )
|
|
312
|
+
const acUnits = []; // window air-conditioner boxes on a random subset of shaft windows
|
|
313
|
+
const finials = []; // pinnacles along the crown
|
|
314
|
+
const extras = []; // bespoke geometry: the base arcade and the setback / roof slabs
|
|
315
|
+
|
|
316
|
+
const addPier = ( frame, u, vBottom, height ) => {
|
|
317
|
+
|
|
318
|
+
const key = Math.round( height * 1000 ); // bucket equal pier heights ( a number key, no string )
|
|
319
|
+
if ( piers.has( key ) === false ) piers.set( key, [] );
|
|
320
|
+
piers.get( key ).push( frame.matrix( u, vBottom, 0 ) );
|
|
321
|
+
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// footprints: full mass, and the inset crown after the setback
|
|
325
|
+
|
|
326
|
+
const footprint = buildFootprint( p.footprint.width, p.footprint.depth, p.chamferWidth, p.chamferCornerX, p.chamferCornerZ );
|
|
327
|
+
const faces = buildFaces( footprint );
|
|
328
|
+
|
|
329
|
+
const inset = p.setbackDepth * p.bayWidth;
|
|
330
|
+
const crownFootprint = buildFootprint(
|
|
331
|
+
Math.max( p.bayWidth * 2, p.footprint.width - inset * 2 ),
|
|
332
|
+
Math.max( p.bayWidth * 2, p.footprint.depth - inset * 2 ),
|
|
333
|
+
Math.max( 0, p.chamferWidth - inset ),
|
|
334
|
+
p.chamferCornerX,
|
|
335
|
+
p.chamferCornerZ
|
|
336
|
+
);
|
|
337
|
+
const crownFaces = buildFaces( crownFootprint );
|
|
338
|
+
|
|
339
|
+
// --- generate the parts -----------------------------------------------
|
|
340
|
+
|
|
341
|
+
const crownCornice = p.stringCourseHeight * 1.6; // the crown's heavy cap; its piers stop below it
|
|
342
|
+
|
|
343
|
+
// shaft and crown are the same facade over different faces, spans and pier heights
|
|
344
|
+
const tiers = [
|
|
345
|
+
{ faces, bottom: baseTop, height: shaftHeight, pierHeight: shaftHeight, ac: acUnits },
|
|
346
|
+
{ faces: crownFaces, bottom: shaftTop, height: crownHeight, pierHeight: crownHeight - crownCornice, ac: null }
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
for ( const t of tiers ) {
|
|
350
|
+
|
|
351
|
+
for ( const frame of t.faces ) {
|
|
352
|
+
|
|
353
|
+
addWindows( frame, windows, glass, glassRooms, t.ac, t.bottom, t.height, p );
|
|
354
|
+
addWall( backWalls, frame, t.bottom, t.bottom + t.height, 0.8, - 0.6 );
|
|
355
|
+
addSpandrelBands( bands, frame, t.bottom, t.height, p );
|
|
356
|
+
addPiers( frame, t.bottom, t.pierHeight, p, addPier );
|
|
357
|
+
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// the base: a gothic arcade, capped by a string course
|
|
363
|
+
for ( const frame of faces ) {
|
|
364
|
+
|
|
365
|
+
addArcade( extras, frame, baseHeight, p );
|
|
366
|
+
addCornice( trim, frame, baseTop - p.stringCourseHeight, p.stringCourseHeight, 0.5 );
|
|
367
|
+
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// periodic string courses banding the shaft
|
|
371
|
+
if ( p.stringCourseEvery > 0 ) {
|
|
372
|
+
|
|
373
|
+
for ( let f = p.stringCourseEvery; f < shaftFloors; f += p.stringCourseEvery ) {
|
|
374
|
+
|
|
375
|
+
for ( const frame of faces ) addCornice( trim, frame, baseTop + f * p.floorHeight - p.stringCourseHeight * 0.5, p.stringCourseHeight, 0.3 );
|
|
376
|
+
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// the crown's heavy cornice, its parapet and the finials along the top
|
|
382
|
+
for ( const frame of crownFaces ) {
|
|
383
|
+
|
|
384
|
+
addCornice( trim, frame, p.totalHeight - crownCornice, crownCornice, 0.9 );
|
|
385
|
+
addParapet( trim, frame, p.totalHeight, p );
|
|
386
|
+
addFinials( frame, finials, shaftTop, crownHeight, p );
|
|
387
|
+
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// thin slabs capping the setback ledge and the roof
|
|
391
|
+
extras.push( slab( footprint, shaftTop, 0.6 ) );
|
|
392
|
+
extras.push( slab( crownFootprint, p.totalHeight, 0.6 ) );
|
|
393
|
+
|
|
394
|
+
// --- bake every part into one geometry ---------------------------------
|
|
395
|
+
|
|
396
|
+
// one mesh = one draw the renderer can't sort, so bake order is draw order: the
|
|
397
|
+
// facade front-to-back, the backing wall last so its hidden fragments never shade.
|
|
398
|
+
|
|
399
|
+
const groups = [
|
|
400
|
+
{ geometry: buildWindowGeometry( p ), matrices: windows, partId: FRAME, rigid: true },
|
|
401
|
+
{ geometry: nonIndexed( buildGlassGeometry( p ) ), matrices: glass, partId: GLASS, rooms: glassRooms, rigid: true },
|
|
402
|
+
{ geometry: _unitBox, matrices: bands, partId: WALL }
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
for ( const [ key, matrices ] of piers ) groups.push( { geometry: buildPierGeometry( p, key / 1000 ), matrices, partId: PIER, rigid: true } );
|
|
406
|
+
|
|
407
|
+
groups.push( { geometry: _unitBox, matrices: trim, partId: WALL } ); // cornices, parapets
|
|
408
|
+
groups.push( { geometry: _unitBox, matrices: acUnits, partId: AC } );
|
|
409
|
+
groups.push( { geometry: nonIndexed( buildFinialGeometry( p ) ), matrices: finials, partId: ORNAMENT, rigid: true } );
|
|
410
|
+
|
|
411
|
+
for ( const geometry of extras ) groups.push( { geometry: nonIndexed( geometry ), matrices: [ _identity ], partId: WALL, rigid: true } ); // base arcade + slabs, in building-local space
|
|
412
|
+
|
|
413
|
+
groups.push( { geometry: _unitBox, matrices: backWalls, partId: WALL } ); // last — hidden behind the facade
|
|
414
|
+
|
|
415
|
+
const geometry = bakeGroups( groups );
|
|
416
|
+
|
|
417
|
+
const mesh = new Mesh( geometry, this.material || new MeshStandardMaterial( { color: 0xddccaa, roughness: 0.9 } ) );
|
|
418
|
+
mesh.name = 'Skyscraper';
|
|
419
|
+
|
|
420
|
+
this.dispose();
|
|
421
|
+
this.mesh = mesh;
|
|
422
|
+
|
|
423
|
+
return mesh;
|
|
424
|
+
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
rebuild() {
|
|
428
|
+
|
|
429
|
+
return this.build();
|
|
430
|
+
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
dispose() {
|
|
434
|
+
|
|
435
|
+
if ( this.mesh === null ) return;
|
|
436
|
+
|
|
437
|
+
this.mesh.geometry.dispose();
|
|
438
|
+
this.mesh = null;
|
|
439
|
+
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// fixed baseline. the remaining parameters (footprint, tierFractions, pierWidth,
|
|
445
|
+
// pierDepth, windowReveal, stringCourseHeight, archBayWidthRatio, archRise) are
|
|
446
|
+
// derived from the seed by randomStyle() unless the caller provides them.
|
|
447
|
+
SkyscraperGenerator.defaults = {
|
|
448
|
+
seed: 35,
|
|
449
|
+
totalHeight: 140,
|
|
450
|
+
floorHeight: 4,
|
|
451
|
+
bayWidth: 2.6,
|
|
452
|
+
stringCourseEvery: 6,
|
|
453
|
+
chamferWidth: 4,
|
|
454
|
+
chamferCornerX: 1,
|
|
455
|
+
chamferCornerZ: 1,
|
|
456
|
+
setbackDepth: 1.5,
|
|
457
|
+
acChance: 0.12
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// --- footprint & faces ---------------------------------------------------
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* A rectangle (centred at the origin in the XZ plane) with one corner cut at
|
|
464
|
+
* 45 degrees, returned as an ordered list of `Vector2( x, z )`. `cornerX` /
|
|
465
|
+
* `cornerZ` ( each ±1 ) pick which corner is cut, so the chamfer can be aimed
|
|
466
|
+
* outward to a block corner.
|
|
467
|
+
*/
|
|
468
|
+
function buildFootprint( width, depth, chamfer, cornerX = 1, cornerZ = 1 ) {
|
|
469
|
+
|
|
470
|
+
const hw = width / 2;
|
|
471
|
+
const hd = depth / 2;
|
|
472
|
+
const c = Math.min( chamfer, hw, hd );
|
|
473
|
+
|
|
474
|
+
// the four corners, counter-clockwise
|
|
475
|
+
const corners = [
|
|
476
|
+
new Vector2( hw, hd ),
|
|
477
|
+
new Vector2( - hw, hd ),
|
|
478
|
+
new Vector2( - hw, - hd ),
|
|
479
|
+
new Vector2( hw, - hd )
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
const points = [];
|
|
483
|
+
|
|
484
|
+
for ( let i = 0; i < corners.length; i ++ ) {
|
|
485
|
+
|
|
486
|
+
const corner = corners[ i ];
|
|
487
|
+
|
|
488
|
+
// cut the requested corner: replace it with two points pulled back along
|
|
489
|
+
// each adjacent edge, leaving a 45° face that points out to that corner
|
|
490
|
+
if ( c > 0 && Math.sign( corner.x ) === cornerX && Math.sign( corner.y ) === cornerZ ) {
|
|
491
|
+
|
|
492
|
+
const prev = corners[ ( i + 3 ) % 4 ];
|
|
493
|
+
const next = corners[ ( i + 1 ) % 4 ];
|
|
494
|
+
points.push( corner.clone().lerp( prev, c / corner.distanceTo( prev ) ) );
|
|
495
|
+
points.push( corner.clone().lerp( next, c / corner.distanceTo( next ) ) );
|
|
496
|
+
|
|
497
|
+
} else {
|
|
498
|
+
|
|
499
|
+
points.push( corner.clone() );
|
|
500
|
+
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return points;
|
|
506
|
+
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Builds a face frame per footprint edge. Each frame is an orthonormal basis
|
|
511
|
+
* ( u along the edge, v up, n outward ) plus an origin and length, so all
|
|
512
|
+
* facade layout can happen in flat ( u, v ) space and bake to world with one
|
|
513
|
+
* matrix — the same authored piece then instances onto every face, including
|
|
514
|
+
* the diagonal chamfer.
|
|
515
|
+
*/
|
|
516
|
+
function buildFaces( points ) {
|
|
517
|
+
|
|
518
|
+
const faces = [];
|
|
519
|
+
const up = new Vector3( 0, 1, 0 );
|
|
520
|
+
|
|
521
|
+
for ( let i = 0; i < points.length; i ++ ) {
|
|
522
|
+
|
|
523
|
+
const a = points[ i ];
|
|
524
|
+
const b = points[ ( i + 1 ) % points.length ];
|
|
525
|
+
|
|
526
|
+
// outward normal: perpendicular to the edge, pointing away from the
|
|
527
|
+
// origin (the footprint is centred there)
|
|
528
|
+
|
|
529
|
+
const n = new Vector3( b.y - a.y, 0, - ( b.x - a.x ) ).normalize();
|
|
530
|
+
const mid = new Vector3( ( a.x + b.x ) / 2, 0, ( a.y + b.y ) / 2 );
|
|
531
|
+
if ( n.dot( mid ) < 0 ) n.negate();
|
|
532
|
+
|
|
533
|
+
// right-handed basis: u = v × n, so makeBasis( u, v, n ) is a pure rotation
|
|
534
|
+
|
|
535
|
+
const u = new Vector3().crossVectors( up, n ).normalize();
|
|
536
|
+
|
|
537
|
+
const pa = new Vector3( a.x, 0, a.y );
|
|
538
|
+
const pb = new Vector3( b.x, 0, b.y );
|
|
539
|
+
const length = pa.distanceTo( pb );
|
|
540
|
+
|
|
541
|
+
// the edge end that u points away from becomes the origin
|
|
542
|
+
|
|
543
|
+
const origin = pb.clone().sub( pa ).dot( u ) > 0 ? pa : pb;
|
|
544
|
+
|
|
545
|
+
faces.push( new FaceFrame( origin, u, up.clone(), n, length ) );
|
|
546
|
+
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return faces;
|
|
550
|
+
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** A face's local ( u along edge, v up, n outward ) frame in world space. */
|
|
554
|
+
class FaceFrame {
|
|
555
|
+
|
|
556
|
+
constructor( origin, u, v, n, length ) {
|
|
557
|
+
|
|
558
|
+
this.origin = origin;
|
|
559
|
+
this.u = u;
|
|
560
|
+
this.v = v;
|
|
561
|
+
this.n = n;
|
|
562
|
+
this.length = length;
|
|
563
|
+
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
point( u, v, w, target = new Vector3() ) {
|
|
567
|
+
|
|
568
|
+
return target
|
|
569
|
+
.copy( this.origin )
|
|
570
|
+
.addScaledVector( this.u, u )
|
|
571
|
+
.addScaledVector( this.v, v )
|
|
572
|
+
.addScaledVector( this.n, w );
|
|
573
|
+
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Places a piece authored in the canonical local frame ( x across, y up, z outward ). */
|
|
577
|
+
matrix( u, v, w ) {
|
|
578
|
+
|
|
579
|
+
return new Matrix4()
|
|
580
|
+
.makeBasis( this.u, this.v, this.n )
|
|
581
|
+
.setPosition( this.point( u, v, w, _point ) );
|
|
582
|
+
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** How many bays of `bayWidth` fit, with the remainder split into end margins. */
|
|
586
|
+
bays( bayWidth ) {
|
|
587
|
+
|
|
588
|
+
const count = Math.max( 1, Math.floor( this.length / bayWidth ) );
|
|
589
|
+
const margin = ( this.length - count * bayWidth ) / 2;
|
|
590
|
+
|
|
591
|
+
return { count, margin, width: bayWidth };
|
|
592
|
+
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// --- shell pieces --------------------------------------------------------
|
|
598
|
+
|
|
599
|
+
// a Matrix4 mapping the shared unit box ( 1×1×1, centred ) onto a face-aligned
|
|
600
|
+
// box of the given size, centred at the given face-local point. these matrices
|
|
601
|
+
// are what the shell InstancedMesh is built from.
|
|
602
|
+
function boxMatrix( frame, u, v, w, sizeU, sizeV, sizeN ) {
|
|
603
|
+
|
|
604
|
+
return new Matrix4()
|
|
605
|
+
.makeBasis( frame.u, frame.v, frame.n )
|
|
606
|
+
.scale( _scale.set( sizeU, sizeV, sizeN ) )
|
|
607
|
+
.setPosition( frame.point( u, v, w, _point ) );
|
|
608
|
+
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function addWall( target, frame, vBottom, vTop, thickness = 0.8, front = 0 ) {
|
|
612
|
+
|
|
613
|
+
const h = vTop - vBottom;
|
|
614
|
+
target.push( boxMatrix( frame, frame.length / 2, vBottom + h / 2, front - thickness / 2, frame.length + thickness * 2, h, thickness ) );
|
|
615
|
+
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Horizontal terracotta bands at every floor line. Together with the projecting
|
|
620
|
+
* piers they form the facade grid; the gaps between them are the window
|
|
621
|
+
* openings, with glass set behind.
|
|
622
|
+
*/
|
|
623
|
+
function addSpandrelBands( target, frame, vBottom, height, p ) {
|
|
624
|
+
|
|
625
|
+
const floors = Math.max( 1, Math.round( height / p.floorHeight ) );
|
|
626
|
+
const fh = height / floors;
|
|
627
|
+
const bandHeight = p.floorHeight - p.windowHeight; // whole courses: floor minus the glazed opening
|
|
628
|
+
|
|
629
|
+
// pull the ends in by the band depth so a band doesn't poke its end-cap
|
|
630
|
+
// into the plane of the perpendicular face at the corners ( overdraw )
|
|
631
|
+
const bandLength = Math.max( 0.2, frame.length - 0.6 );
|
|
632
|
+
|
|
633
|
+
for ( let f = 0; f <= floors; f ++ ) {
|
|
634
|
+
|
|
635
|
+
// front flush at w = 0, meeting the backing wall behind
|
|
636
|
+
target.push( boxMatrix( frame, frame.length / 2, vBottom + f * fh, - 0.3, bandLength, bandHeight, 0.6 ) );
|
|
637
|
+
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* A thin horizontal cap over a footprint's bounding box at height `y`. Its
|
|
644
|
+
* sides are pulled in behind the facade plane ( into the backing-wall shell )
|
|
645
|
+
* so they never sit coplanar with the walls, spandrels or piers and z-fight.
|
|
646
|
+
*/
|
|
647
|
+
function slab( footprint, y, thickness ) {
|
|
648
|
+
|
|
649
|
+
// a thin cap following the footprint OUTLINE ( so the chamfered corner is cut, not
|
|
650
|
+
// left overhanging as a rectangular box ), inset a little so its edge tucks just
|
|
651
|
+
// behind the facade and the wall top reads as a lip around it
|
|
652
|
+
|
|
653
|
+
const inset = 0.8;
|
|
654
|
+
let cx = 0, cz = 0;
|
|
655
|
+
for ( const p of footprint ) {
|
|
656
|
+
|
|
657
|
+
cx += p.x; cz += p.y;
|
|
658
|
+
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
cx /= footprint.length; cz /= footprint.length;
|
|
662
|
+
|
|
663
|
+
// consistent ( CCW ) winding so the extrude caps face up / down correctly
|
|
664
|
+
let area = 0;
|
|
665
|
+
for ( let i = 0; i < footprint.length; i ++ ) {
|
|
666
|
+
|
|
667
|
+
const a = footprint[ i ], b = footprint[ ( i + 1 ) % footprint.length ];
|
|
668
|
+
area += a.x * b.y - b.x * a.y;
|
|
669
|
+
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const pts = area < 0 ? footprint.slice().reverse() : footprint;
|
|
673
|
+
|
|
674
|
+
const shape = new Shape();
|
|
675
|
+
pts.forEach( ( p, i ) => {
|
|
676
|
+
|
|
677
|
+
const dx = cx - p.x, dz = cz - p.y;
|
|
678
|
+
const d = Math.hypot( dx, dz ) || 1;
|
|
679
|
+
const x = p.x + dx / d * inset;
|
|
680
|
+
const z = p.y + dz / d * inset;
|
|
681
|
+
if ( i === 0 ) shape.moveTo( x, z ); else shape.lineTo( x, z );
|
|
682
|
+
|
|
683
|
+
} );
|
|
684
|
+
|
|
685
|
+
// extrude the XZ outline downward by the thickness, the top dropped just below height y:
|
|
686
|
+
// the inset cap would otherwise sit coplanar with the surrounding wall top faces and
|
|
687
|
+
// z-fight, and the parapet / spandrel bands around the edge hide the shallow recess
|
|
688
|
+
const drop = 0.2;
|
|
689
|
+
const geometry = new ExtrudeGeometry( shape, { depth: thickness, bevelEnabled: false } );
|
|
690
|
+
geometry.rotateX( Math.PI / 2 );
|
|
691
|
+
geometry.translate( 0, y - drop, 0 );
|
|
692
|
+
return geometry;
|
|
693
|
+
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** A two-step projecting cornice / string-course band wrapping a face. */
|
|
697
|
+
function addCornice( target, frame, vBottom, height, depth ) {
|
|
698
|
+
|
|
699
|
+
target.push( boxMatrix( frame, frame.length / 2, vBottom + height * 0.275, depth / 2, frame.length, height * 0.55, depth ) );
|
|
700
|
+
target.push( boxMatrix( frame, frame.length / 2, vBottom + height * 0.775, depth * 0.85, frame.length, height * 0.45, depth * 1.7 ) );
|
|
701
|
+
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** A low parapet wall capping the crown. */
|
|
705
|
+
function addParapet( target, frame, vTop, p ) {
|
|
706
|
+
|
|
707
|
+
const height = 1.4;
|
|
708
|
+
target.push( boxMatrix( frame, frame.length / 2, vTop + height / 2, p.pierDepth * 0.4, frame.length, height, p.pierDepth * 0.8 ) );
|
|
709
|
+
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* The base storey: a wall pierced by tall pointed-arch openings, extruded with
|
|
714
|
+
* thickness so the openings read as deep recesses.
|
|
715
|
+
*/
|
|
716
|
+
function addArcade( target, frame, height, p ) {
|
|
717
|
+
|
|
718
|
+
const archWidth = p.bayWidth * p.archBayWidthRatio;
|
|
719
|
+
const { count, margin } = frame.bays( archWidth );
|
|
720
|
+
|
|
721
|
+
const sill = height * 0.04;
|
|
722
|
+
const spring = height * 0.55;
|
|
723
|
+
const apex = Math.min( height * 0.96, spring + ( archWidth / 2 ) * ( 0.8 + p.archRise ) );
|
|
724
|
+
|
|
725
|
+
const shape = new Shape();
|
|
726
|
+
shape.moveTo( 0, 0 );
|
|
727
|
+
shape.lineTo( frame.length, 0 );
|
|
728
|
+
shape.lineTo( frame.length, height );
|
|
729
|
+
shape.lineTo( 0, height );
|
|
730
|
+
shape.lineTo( 0, 0 );
|
|
731
|
+
|
|
732
|
+
for ( let i = 0; i < count; i ++ ) {
|
|
733
|
+
|
|
734
|
+
const cx = margin + ( i + 0.5 ) * archWidth;
|
|
735
|
+
const hw = archWidth * 0.34;
|
|
736
|
+
|
|
737
|
+
const hole = new Path();
|
|
738
|
+
hole.moveTo( cx - hw, sill );
|
|
739
|
+
hole.lineTo( cx - hw, spring );
|
|
740
|
+
hole.quadraticCurveTo( cx - hw, apex, cx, apex );
|
|
741
|
+
hole.quadraticCurveTo( cx + hw, apex, cx + hw, spring );
|
|
742
|
+
hole.lineTo( cx + hw, sill );
|
|
743
|
+
hole.lineTo( cx - hw, sill );
|
|
744
|
+
shape.holes.push( hole );
|
|
745
|
+
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const thickness = 1.1;
|
|
749
|
+
const geometry = new ExtrudeGeometry( shape, { depth: thickness, bevelEnabled: false, curveSegments: 8 } );
|
|
750
|
+
geometry.translate( 0, 0, - thickness );
|
|
751
|
+
geometry.applyMatrix4( frame.matrix( 0, 0, 0 ) );
|
|
752
|
+
|
|
753
|
+
target.push( geometry );
|
|
754
|
+
|
|
755
|
+
// a dark plane set behind the openings so the recesses read
|
|
756
|
+
|
|
757
|
+
const back = new PlaneGeometry( frame.length, height );
|
|
758
|
+
back.applyMatrix4( frame.matrix( frame.length / 2, height / 2, - thickness - 0.4 ) );
|
|
759
|
+
target.push( back );
|
|
760
|
+
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// --- repeating field -----------------------------------------------------
|
|
764
|
+
|
|
765
|
+
function addPiers( frame, vBottom, height, p, addPier ) {
|
|
766
|
+
|
|
767
|
+
const { count, margin, width } = frame.bays( p.bayWidth );
|
|
768
|
+
|
|
769
|
+
// a pier on every bay edge except the far end: that corner is shared with
|
|
770
|
+
// the next face, which places its own pier there, so emitting both would
|
|
771
|
+
// stack two piers at each corner
|
|
772
|
+
|
|
773
|
+
for ( let i = 0; i < count; i ++ ) {
|
|
774
|
+
|
|
775
|
+
addPier( frame, margin + i * width, vBottom, height );
|
|
776
|
+
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function addWindows( frame, windows, glass, glassRooms, acUnits, vBottom, height, p ) {
|
|
782
|
+
|
|
783
|
+
const { count, margin, width } = frame.bays( p.bayWidth );
|
|
784
|
+
const floors = Math.max( 1, Math.round( height / p.floorHeight ) );
|
|
785
|
+
const fh = height / floors;
|
|
786
|
+
|
|
787
|
+
// a window AC unit sitting on the sill, protruding from the facade. about half the window
|
|
788
|
+
// width, capped at a real unit's size ( ~0.66 m ) and kept wider than tall, sticking out
|
|
789
|
+
// about half its width
|
|
790
|
+
const acW = Math.min( ( p.bayWidth - p.pierWidth ) * 0.55, 0.66 );
|
|
791
|
+
const acH = acW * 0.6;
|
|
792
|
+
const acD = acW * 0.5;
|
|
793
|
+
const acV = - p.windowHeight / 2 + acH / 2 + WINDOW_BORDER; // bottom rests on the sill ( the top of the window's bottom frame rail )
|
|
794
|
+
|
|
795
|
+
// a real ~0.66 m unit looks lost in a wide opening, so only fit ACs where it still spans a
|
|
796
|
+
// fair share of the window — in practice, the narrower ( older-style ) windows
|
|
797
|
+
const acFits = acW >= ( width - p.pierWidth ) * 0.34;
|
|
798
|
+
|
|
799
|
+
for ( let f = 0; f < floors; f ++ ) {
|
|
800
|
+
|
|
801
|
+
const cy = vBottom + ( f + 0.5 ) * fh;
|
|
802
|
+
|
|
803
|
+
// the interior-mapping room module: one floor tall, a run of two or three bays
|
|
804
|
+
// wide, chosen per floor so neighbouring windows share an interior. the choice
|
|
805
|
+
// is deterministic ( seeded by the floor and the face ) so it is stable, and the
|
|
806
|
+
// run is recorded per window so the material can ray-march the right box.
|
|
807
|
+
const roomBays = floorHash( f, frame, 0 ) > 0.5 ? 3 : 2;
|
|
808
|
+
const roomPhase = Math.floor( floorHash( f, frame, 1 ) * roomBays );
|
|
809
|
+
|
|
810
|
+
for ( let b = 0; b < count; b ++ ) {
|
|
811
|
+
|
|
812
|
+
const cx = margin + ( b + 0.5 ) * width;
|
|
813
|
+
|
|
814
|
+
windows.push( frame.matrix( cx, cy, 0 ) );
|
|
815
|
+
glass.push( frame.matrix( cx, cy, - p.windowReveal ) );
|
|
816
|
+
|
|
817
|
+
// the run of bays this window's room spans, clamped at the face ends, recorded
|
|
818
|
+
// as the room's centre on the facade and its width × height in metres
|
|
819
|
+
const room = Math.floor( ( b + roomPhase ) / roomBays );
|
|
820
|
+
const bStart = Math.max( 0, room * roomBays - roomPhase );
|
|
821
|
+
const bEnd = Math.min( count, ( room + 1 ) * roomBays - roomPhase );
|
|
822
|
+
const span = bEnd - bStart;
|
|
823
|
+
glassRooms.push( { center: frame.point( margin + ( bStart + span / 2 ) * width, cy, - p.windowReveal ), size: new Vector2( span * width, fh - 1 ) } ); // centred on the glass plane, so the interior is anchored to the pane it is drawn on
|
|
824
|
+
|
|
825
|
+
if ( acUnits && acFits ) {
|
|
826
|
+
|
|
827
|
+
// deterministic per-window hash ( varies per face via the frame origin )
|
|
828
|
+
const r = Math.sin( f * 41.3 + b * 12.7 + frame.origin.x * 0.13 + frame.origin.z * 0.31 ) * 43758.5453;
|
|
829
|
+
// the back tucks into the window reveal ( just in front of the glass ) so the unit sits
|
|
830
|
+
// in the opening instead of floating on the facade
|
|
831
|
+
const acW0 = acD / 2 - p.windowReveal + 0.04;
|
|
832
|
+
if ( r - Math.floor( r ) < p.acChance ) acUnits.push( boxMatrix( frame, cx, cy + acV, acW0, acW, acH, acD ) );
|
|
833
|
+
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function addFinials( frame, finials, vBottom, height, p ) {
|
|
843
|
+
|
|
844
|
+
const { count, margin, width } = frame.bays( p.bayWidth );
|
|
845
|
+
const top = vBottom + height;
|
|
846
|
+
|
|
847
|
+
// skip the far-end bay edge: it is the shared corner the next face also
|
|
848
|
+
// caps, so emitting both would stack two finials at each corner
|
|
849
|
+
|
|
850
|
+
for ( let i = 0; i < count; i ++ ) {
|
|
851
|
+
|
|
852
|
+
finials.push( new Matrix4().setPosition( frame.point( margin + i * width, top, p.pierDepth * 0.5, _point ) ) );
|
|
853
|
+
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// --- authored modules ----------------------------------------------------
|
|
859
|
+
|
|
860
|
+
function buildPierGeometry( p, height ) {
|
|
861
|
+
|
|
862
|
+
// a wide pier with a slimmer pilaster raised on its face, giving the
|
|
863
|
+
// continuous vertical rib a stepped, terracotta profile
|
|
864
|
+
|
|
865
|
+
const back = new BoxGeometry( p.pierWidth, height, p.pierDepth * 0.6 );
|
|
866
|
+
back.translate( 0, height / 2, p.pierDepth * 0.3 );
|
|
867
|
+
|
|
868
|
+
// the pilaster stops just short of the pier top so that where a pier is left
|
|
869
|
+
// exposed ( at a setback ) the cap reads as one clean block rather than the
|
|
870
|
+
// back box and the pilaster stacked into a T
|
|
871
|
+
const pilasterHeight = Math.max( 1, height - 0.6 );
|
|
872
|
+
const front = new BoxGeometry( p.pierWidth * 0.55, pilasterHeight, p.pierDepth * 0.45 );
|
|
873
|
+
front.translate( 0, pilasterHeight / 2, p.pierDepth * 0.6 + p.pierDepth * 0.225 );
|
|
874
|
+
|
|
875
|
+
return merge( [ back, front ] );
|
|
876
|
+
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function buildWindowGeometry( p ) {
|
|
880
|
+
|
|
881
|
+
// the flat frame face ( a rectangle with the glazing hole ), the four reveal walls
|
|
882
|
+
// of the opening and the glazing bars, merged into one instanced module. a full
|
|
883
|
+
// extrusion would also emit a hidden back cap and outer side walls; windows are by
|
|
884
|
+
// far the heaviest part of a building, so those are skipped.
|
|
885
|
+
|
|
886
|
+
const w = p.bayWidth - p.pierWidth;
|
|
887
|
+
const h = p.windowHeight;
|
|
888
|
+
const border = WINDOW_BORDER;
|
|
889
|
+
const depth = p.windowReveal; // reveal walls run all the way back to the glass ( placed at -windowReveal ), so no gap opens between them and the pane
|
|
890
|
+
const iw = w / 2 - border;
|
|
891
|
+
const ih = h / 2 - border;
|
|
892
|
+
|
|
893
|
+
const shape = new Shape();
|
|
894
|
+
shape.moveTo( - w / 2, - h / 2 );
|
|
895
|
+
shape.lineTo( w / 2, - h / 2 );
|
|
896
|
+
shape.lineTo( w / 2, h / 2 );
|
|
897
|
+
shape.lineTo( - w / 2, h / 2 );
|
|
898
|
+
shape.lineTo( - w / 2, - h / 2 );
|
|
899
|
+
|
|
900
|
+
const hole = new Path();
|
|
901
|
+
hole.moveTo( - iw, - ih );
|
|
902
|
+
hole.lineTo( - iw, ih );
|
|
903
|
+
hole.lineTo( iw, ih );
|
|
904
|
+
hole.lineTo( iw, - ih );
|
|
905
|
+
hole.lineTo( - iw, - ih );
|
|
906
|
+
shape.holes.push( hole );
|
|
907
|
+
|
|
908
|
+
const front = new ShapeGeometry( shape ); // visible frame face, flush with the facade
|
|
909
|
+
|
|
910
|
+
// the four reveal walls of the opening, set back to the glazing
|
|
911
|
+
const wall = ( x, y, rx, ry, sw, sh ) => {
|
|
912
|
+
|
|
913
|
+
const pl = new PlaneGeometry( sw, sh );
|
|
914
|
+
pl.rotateX( rx );
|
|
915
|
+
pl.rotateY( ry );
|
|
916
|
+
pl.translate( x, y, - depth / 2 );
|
|
917
|
+
return pl;
|
|
918
|
+
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
const left = wall( - iw, 0, 0, Math.PI / 2, depth, ih * 2 );
|
|
922
|
+
const right = wall( iw, 0, 0, - Math.PI / 2, depth, ih * 2 );
|
|
923
|
+
const sill = wall( 0, - ih, - Math.PI / 2, 0, iw * 2, depth );
|
|
924
|
+
const head = wall( 0, ih, Math.PI / 2, 0, iw * 2, depth );
|
|
925
|
+
|
|
926
|
+
// a single horizontal glazing bar ( transom ), flat, just in front of the glass —
|
|
927
|
+
// a thin box would triple the window's triangle count for sub-pixel thickness
|
|
928
|
+
const transom = new PlaneGeometry( iw * 2, 0.05 );
|
|
929
|
+
transom.translate( 0, h * 0.04, - depth + 0.02 ); // meeting rail, just above centre
|
|
930
|
+
|
|
931
|
+
return merge( [ front, left, right, sill, head, transom ] );
|
|
932
|
+
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function buildGlassGeometry( p ) {
|
|
936
|
+
|
|
937
|
+
const w = p.bayWidth - p.pierWidth - WINDOW_BORDER * 2;
|
|
938
|
+
const h = p.windowHeight - WINDOW_BORDER * 2;
|
|
939
|
+
|
|
940
|
+
return new PlaneGeometry( w, h );
|
|
941
|
+
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function buildFinialGeometry( p ) {
|
|
945
|
+
|
|
946
|
+
// a tapering pinnacle revolved around its axis
|
|
947
|
+
|
|
948
|
+
const s = p.pierWidth;
|
|
949
|
+
const profile = [
|
|
950
|
+
new Vector2( 0.0, 0 ),
|
|
951
|
+
new Vector2( s * 0.9, 0 ),
|
|
952
|
+
new Vector2( s * 0.9, s * 0.4 ),
|
|
953
|
+
new Vector2( s * 0.55, s * 1.0 ),
|
|
954
|
+
new Vector2( 0.0, s * 3.2 )
|
|
955
|
+
];
|
|
956
|
+
|
|
957
|
+
return new LatheGeometry( profile, 8 ); // round enough to read as a smooth pinnacle, still light
|
|
958
|
+
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// --- material ------------------------------------------------------------
|
|
962
|
+
|
|
963
|
+
// derivative-based bump for a procedural, world-space height field. the built-in bumpMap
|
|
964
|
+
// offsets the UV to read its height, so it returns a zero gradient for a height keyed off
|
|
965
|
+
// world position; this feeds the hardware screen-space derivatives of the height into
|
|
966
|
+
// Mikkelsen's surface-gradient method so the relief actually perturbs the normal.
|
|
967
|
+
function bumpNormal( height ) {
|
|
968
|
+
|
|
969
|
+
const dpdx = positionView.dFdx();
|
|
970
|
+
const dpdy = positionView.dFdy();
|
|
971
|
+
const r1 = dpdy.cross( normalView );
|
|
972
|
+
const r2 = normalView.cross( dpdx );
|
|
973
|
+
const det = dpdx.dot( r1 );
|
|
974
|
+
const grad = det.sign().mul( height.dFdx().mul( r1 ).add( height.dFdy().mul( r2 ) ) );
|
|
975
|
+
|
|
976
|
+
return det.abs().mul( normalView ).sub( grad ).normalize();
|
|
977
|
+
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// interior mapping: fakes a furnished room behind each glass pane in the fragment
|
|
981
|
+
// shader — no geometry, no texture. every pane carries the room it looks into ( centre +
|
|
982
|
+
// size, baked per window by addWindows ), so neighbouring panes share one interior. the
|
|
983
|
+
// view ray is cast into that box and the walls, floor, ceiling and a few furniture pieces
|
|
984
|
+
// it meets are shaded procedurally, keyed off a per-room hash. returns vec4( colour, lit ).
|
|
985
|
+
const interior = /*@__PURE__*/ Fn( () => {
|
|
986
|
+
|
|
987
|
+
// flat so floor() below can't split one pane across two cell ids ( centre is per-room )
|
|
988
|
+
const roomCenter = varying( attribute( 'roomCenter', 'vec3' ) ).setInterpolation( InterpolationSamplingType.FLAT, InterpolationSamplingMode.EITHER );
|
|
989
|
+
const roomSize = attribute( 'roomSize', 'vec2' );
|
|
990
|
+
|
|
991
|
+
// a per-face frame from the geometry normal ( holds on every facade, including the
|
|
992
|
+
// 45° chamfer ): u runs across the face, v is up, n points outward
|
|
993
|
+
const n = normalLocal;
|
|
994
|
+
const up = vec3( 0, 1, 0 );
|
|
995
|
+
const uAxis = cross( up, n ).normalize();
|
|
996
|
+
|
|
997
|
+
// this pixel and the view ray, in the room's ( across, up, depth ) frame; depth
|
|
998
|
+
// runs into the wall, so the ray's depth component is positive
|
|
999
|
+
const d = positionLocal.sub( roomCenter );
|
|
1000
|
+
const camLocal = modelWorldMatrixInverse.mul( vec4( cameraPosition, 1 ) ).xyz;
|
|
1001
|
+
const rayLocal = positionLocal.sub( camLocal ).normalize();
|
|
1002
|
+
const origin = vec3( dot( d, uAxis ), d.y, 0 );
|
|
1003
|
+
const dir = vec3( dot( rayLocal, uAxis ), rayLocal.y, dot( rayLocal, n ).negate() );
|
|
1004
|
+
|
|
1005
|
+
// the room box: the pane-wide × ceiling-height front rectangle ( centred on the pane ),
|
|
1006
|
+
// set back behind the glass and run a little deeper than it is tall. shade the far
|
|
1007
|
+
// side the ray exits ( slab method: nearest of the three far-plane crossings;
|
|
1008
|
+
// dividing by a near-zero direction gives ±inf, which min() harmlessly drops ).
|
|
1009
|
+
const setback = float( 0.1 ); // the room starts just behind the glass, so it sits flush in the frame opening
|
|
1010
|
+
const boxMax = vec3( roomSize.x.mul( 0.5 ), roomSize.y.mul( 0.5 ), setback.add( roomSize.y.mul( 1.55 ) ) );
|
|
1011
|
+
const boxMin = vec3( boxMax.x.negate(), boxMax.y.negate(), setback );
|
|
1012
|
+
const tFar = boxMin.sub( origin ).div( dir ).max( boxMax.sub( origin ).div( dir ) );
|
|
1013
|
+
const t = tFar.x.min( tFar.y ).min( tFar.z );
|
|
1014
|
+
const hit = origin.add( dir.mul( t ) );
|
|
1015
|
+
const q = hit.sub( boxMin ).div( boxMax.sub( boxMin ) ); // 0..1 inside the room
|
|
1016
|
+
|
|
1017
|
+
const onBack = q.z.greaterThan( 0.998 );
|
|
1018
|
+
const onCeil = q.y.greaterThan( 0.998 );
|
|
1019
|
+
const onFloor = q.y.lessThan( 0.002 );
|
|
1020
|
+
|
|
1021
|
+
// per-room key for a portable integer hash — fract( sin() ) isn't bit-exact across drivers
|
|
1022
|
+
const cell = floor( roomCenter.mul( 2.0 ) ); // + offset before the u32 cast keeps it non-negative
|
|
1023
|
+
const ckey = uint( cell.x.add( 1 << 21 ) ).mul( uint( 73856093 ) )
|
|
1024
|
+
.bitXor( uint( cell.y.add( 1 << 21 ) ).mul( uint( 19349663 ) ) )
|
|
1025
|
+
.bitXor( uint( cell.z.add( 1 << 21 ) ).mul( uint( 83492791 ) ) ).toVar();
|
|
1026
|
+
const hash = ( kx, ky, kz ) => ihash( ckey.add( uint( Math.round( ( kx + ky * 7 + kz * 13 ) * 100 ) ) ) );
|
|
1027
|
+
const seed = hash( 12.9898, 78.233, 37.719 );
|
|
1028
|
+
const seed2 = hash( 39.346, 11.135, 83.155 );
|
|
1029
|
+
const lit = step( 0.8, hash( 63.21, 9.17, 51.43 ) ); // ~20% of rooms have the lights on; the rest sit dark
|
|
1030
|
+
|
|
1031
|
+
// each room's bulb colour. most run warm, drifting from a dim amber ( ~2400K ) up to a
|
|
1032
|
+
// warm white ( ~3200K ); a minority run cool, from a fluorescent / LED daylight to a TV's
|
|
1033
|
+
// bluer glow — so a lit facade reads as a spread of bulb temperatures, not one flat tint
|
|
1034
|
+
const warmLight = mix( color( 0xffb845 ), color( 0xffe49c ), hash( 27.1, 4.9, 61.7 ) );
|
|
1035
|
+
const coolLight = mix( color( 0xdfe8ff ), color( 0x9fb6ff ), hash( 8.3, 51.2, 17.6 ) );
|
|
1036
|
+
const lightCol = select( hash( 44.7, 19.3, 6.1 ).greaterThan( 0.88 ), coolLight, warmLight ); // ~12% of lit rooms run cool
|
|
1037
|
+
|
|
1038
|
+
// depth falloff ( darker toward the back ), and a panel mask on a face given its
|
|
1039
|
+
// two 0..1 coordinates — used for the flat fittings below
|
|
1040
|
+
const depth = roomSize.y.mul( 1.55 );
|
|
1041
|
+
const falloffAt = ( z ) => mix( float( 1.0 ), float( 0.42 ), z.sub( setback ).div( depth ).clamp( 0, 1 ) );
|
|
1042
|
+
const rect = ( ax, ay, cx, cy, hw, hh ) => smoothstep( hw + 0.006, hw - 0.006, ax.sub( cx ).abs() ).mul( smoothstep( hh + 0.006, hh - 0.006, ay.sub( cy ).abs() ) );
|
|
1043
|
+
|
|
1044
|
+
// --- the room shell: walls, floor, ceiling, back wall, with flat fittings ----
|
|
1045
|
+
|
|
1046
|
+
// muted plaster, picked per room, with a darker skirting board along the wall foot
|
|
1047
|
+
let wall = mix( color( 0x9a8b73 ), color( 0x6f7a82 ), seed );
|
|
1048
|
+
wall = mix( wall, color( 0xb9ad97 ), seed2.mul( 0.6 ) );
|
|
1049
|
+
const wallCol = mix( wall, wall.mul( 0.5 ), smoothstep( 0.05, 0.04, q.y ) );
|
|
1050
|
+
|
|
1051
|
+
// floorboards with a thin seam every few, and a centred rug
|
|
1052
|
+
const seam = step( 0.94, fract( q.x.mul( 6 ) ) );
|
|
1053
|
+
const boards = mix( color( 0x4a3320 ), color( 0x6a4c30 ), seed ).mul( seam.mul( 0.3 ).oneMinus() );
|
|
1054
|
+
const rug = mix( color( 0x7a3b32 ), color( 0x3a5760 ), seed2 );
|
|
1055
|
+
const floorCol = mix( boards, rug, rect( q.x, q.z, 0.5, 0.62, 0.3, 0.26 ).mul( 0.9 ) );
|
|
1056
|
+
|
|
1057
|
+
// ceiling, lighter than the walls, with a round overhead light in the middle; in a
|
|
1058
|
+
// lit room the fixture reads bright and glows ( the material's emissive = colour × lit )
|
|
1059
|
+
const lamp = smoothstep( 0.16, 0.13, vec2( q.x.sub( 0.5 ), q.z.sub( 0.5 ) ).length() );
|
|
1060
|
+
const ceilCol = mix( mix( wall, color( 0xffffff ), 0.5 ), lightCol.mul( mix( float( 1.0 ), float( 4.5 ), lit ) ), lamp );
|
|
1061
|
+
|
|
1062
|
+
// back wall: a panelled door to one side, and a framed picture kept on the
|
|
1063
|
+
// opposite half of the wall so it never lands on the door
|
|
1064
|
+
const doorX = mix( float( 0.22 ), float( 0.78 ), seed );
|
|
1065
|
+
const door = mix( color( 0x5a4631 ), color( 0x39383c ), step( 0.5, seed2 ) );
|
|
1066
|
+
const picX = select( doorX.lessThan( 0.5 ), mix( float( 0.68 ), float( 0.82 ), seed2 ), mix( float( 0.18 ), float( 0.32 ), seed2 ) );
|
|
1067
|
+
const picCol = mix( color( 0x2c3a4a ), color( 0x7a5a3a ), hash( 5.1, 9.2, 3.3 ) );
|
|
1068
|
+
let backCol = mix( wallCol, door, rect( q.x, q.y, doorX, 0.33, 0.085, 0.35 ) );
|
|
1069
|
+
backCol = mix( backCol, color( 0x141210 ), rect( q.x, q.y, picX, 0.56, 0.075, 0.085 ) ); // dark frame
|
|
1070
|
+
backCol = mix( backCol, picCol, rect( q.x, q.y, picX, 0.56, 0.055, 0.065 ) ); // the picture
|
|
1071
|
+
|
|
1072
|
+
const shellCol = select( onBack, backCol, select( onCeil, ceilCol, select( onFloor, floorCol, wallCol ) ) );
|
|
1073
|
+
|
|
1074
|
+
// fake ambient occlusion: darken the hit toward the room's edges ( where two surfaces
|
|
1075
|
+
// meet ), so the box reads with soft corner shading instead of flat-lit walls. the two
|
|
1076
|
+
// in-plane axes depend on which face the ray exits through ( q is 0..1 inside the room ).
|
|
1077
|
+
const aoBand = 0.15;
|
|
1078
|
+
const aoEdge = ( a ) => smoothstep( 0, aoBand, a ).mul( smoothstep( 0, aoBand, a.oneMinus() ) );
|
|
1079
|
+
const edgeAO = select( onBack, aoEdge( q.x ).mul( aoEdge( q.y ) ), select( onFloor.or( onCeil ), aoEdge( q.x ).mul( aoEdge( q.z ) ), aoEdge( q.y ).mul( aoEdge( q.z ) ) ) );
|
|
1080
|
+
const shellAO = mix( float( 0.72 ), float( 1.0 ), edgeAO );
|
|
1081
|
+
|
|
1082
|
+
// --- nearest surface: the shell, then any furniture block that lies closer ----
|
|
1083
|
+
// each block is a solid axis-aligned box in room space; boxHit returns its near
|
|
1084
|
+
// face. consider() keeps whichever surface the ray meets first.
|
|
1085
|
+
let bestT = t;
|
|
1086
|
+
let bestCol = shellCol.mul( shellAO ).mul( falloffAt( hit.z ) );
|
|
1087
|
+
let bestEmit = float( 1 ); // per-hit emissive weight: shell and fittings emit fully, curtains far less
|
|
1088
|
+
|
|
1089
|
+
const boxHit = ( bMin, bMax ) => {
|
|
1090
|
+
|
|
1091
|
+
const ta = bMin.sub( origin ).div( dir );
|
|
1092
|
+
const tb = bMax.sub( origin ).div( dir );
|
|
1093
|
+
const lo = ta.min( tb ), hi = ta.max( tb );
|
|
1094
|
+
const tN = lo.x.max( lo.y ).max( lo.z );
|
|
1095
|
+
const p = origin.add( dir.mul( tN ) );
|
|
1096
|
+
return { tN, p, hit: hi.x.min( hi.y ).min( hi.z ).greaterThan( tN ).and( tN.greaterThan( 0 ) ), qb: p.sub( bMin ).div( bMax.sub( bMin ) ) };
|
|
1097
|
+
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const consider = ( h, tN, c, emit = 1 ) => {
|
|
1101
|
+
|
|
1102
|
+
const near = h.and( tN.lessThan( bestT ) ); bestCol = select( near, c, bestCol ); bestEmit = select( near, float( emit ), bestEmit ); bestT = select( near, tN, bestT );
|
|
1103
|
+
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
const halfU = boxMax.x, floorY = boxMin.y, ceilY = boxMax.y, backZ = boxMax.z;
|
|
1107
|
+
const midZ = setback.add( depth.mul( 0.5 ) ); // room centre, in depth
|
|
1108
|
+
|
|
1109
|
+
// a low table near the middle of the room ( its top catches the light )
|
|
1110
|
+
const tCx = mix( float( - 0.6 ), float( 0.6 ), seed );
|
|
1111
|
+
const tCz = midZ.add( mix( float( - 0.4 ), float( 0.5 ), seed2 ) );
|
|
1112
|
+
const tbl = boxHit( vec3( tCx.sub( 0.6 ), floorY, tCz.sub( 0.35 ) ), vec3( tCx.add( 0.6 ), floorY.add( 0.42 ), tCz.add( 0.35 ) ) );
|
|
1113
|
+
const tblCol = mix( color( 0x4a3526 ), color( 0x6b4a30 ), seed2 ).mul( select( tbl.qb.y.greaterThan( 0.94 ), float( 1.25 ), float( 0.8 ) ) );
|
|
1114
|
+
consider( tbl.hit, tbl.tN, tblCol.mul( falloffAt( tbl.p.z ) ) );
|
|
1115
|
+
|
|
1116
|
+
// a wide low sofa against the back wall, facing the window
|
|
1117
|
+
const sofaCx = mix( halfU.mul( - 0.3 ), halfU.mul( 0.3 ), seed2 );
|
|
1118
|
+
const sofa = boxHit( vec3( sofaCx.sub( 1.1 ), floorY, backZ.sub( 0.95 ) ), vec3( sofaCx.add( 1.1 ), floorY.add( mix( float( 0.8 ), float( 0.9 ), seed ) ), backZ.sub( 0.1 ) ) );
|
|
1119
|
+
const sofaCol = mix( color( 0x5a4a3a ), color( 0x42566a ), seed ).mul( select( sofa.qb.y.greaterThan( 0.9 ), float( 1.12 ), float( 0.85 ) ) );
|
|
1120
|
+
consider( sofa.hit, sofa.tN, sofaCol.mul( falloffAt( sofa.p.z ) ) );
|
|
1121
|
+
|
|
1122
|
+
// tall wardrobes in the back corners — each side stands in some rooms
|
|
1123
|
+
const wardrobe = ( cx, gate, h ) => {
|
|
1124
|
+
|
|
1125
|
+
const w = boxHit( vec3( cx.sub( 0.5 ), floorY, backZ.sub( 0.7 ) ), vec3( cx.add( 0.5 ), floorY.add( h ), backZ.sub( 0.1 ) ) );
|
|
1126
|
+
const c = mix( color( 0x3a2c22 ), color( 0x55473a ), seed ).mul( select( w.qb.y.greaterThan( 0.94 ), float( 1.2 ), float( 0.82 ) ) );
|
|
1127
|
+
consider( w.hit.and( gate ), w.tN, c.mul( falloffAt( w.p.z ) ) );
|
|
1128
|
+
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
wardrobe( halfU.mul( - 0.82 ), hash( 7.3, 2.1, 9.9 ).greaterThan( 0.4 ), mix( float( 1.7 ), float( 2.3 ), seed ) );
|
|
1132
|
+
wardrobe( halfU.mul( 0.82 ), hash( 3.7, 8.4, 1.5 ).greaterThan( 0.4 ), mix( float( 1.7 ), float( 2.3 ), seed2 ) );
|
|
1133
|
+
|
|
1134
|
+
// curtains hung just inside the glass: drapes drawn part-way in from each side,
|
|
1135
|
+
// so some windows read open and others half-covered
|
|
1136
|
+
|
|
1137
|
+
// curtain fabric colour, picked per room from a muted domestic palette — creams and
|
|
1138
|
+
// taupes through warm grey, dusty blue, sage and faded terracotta — with a small
|
|
1139
|
+
// in-family drift so drawn drapes vary window to window instead of all reading beige
|
|
1140
|
+
const swatch = ( a, b ) => mix( color( a ), color( b ), seed2 );
|
|
1141
|
+
const pick = hash( 22.4, 6.7, 91.2 ).mul( 6 ); // 0..6, one bucket per family
|
|
1142
|
+
let fabric = swatch( 0xcabfa6, 0xd8cdb8 ); // cream
|
|
1143
|
+
fabric = select( pick.greaterThan( 1 ), swatch( 0x8a7a64, 0x9b8c72 ), fabric ); // beige / taupe
|
|
1144
|
+
fabric = select( pick.greaterThan( 2 ), swatch( 0x706a64, 0x837d76 ), fabric ); // warm grey
|
|
1145
|
+
fabric = select( pick.greaterThan( 3 ), swatch( 0x5f7079, 0x6f818b ), fabric ); // dusty blue
|
|
1146
|
+
fabric = select( pick.greaterThan( 4 ), swatch( 0x6c7558, 0x79835f ), fabric ); // sage green
|
|
1147
|
+
fabric = select( pick.greaterThan( 5 ), swatch( 0x8c5a44, 0x9a6a52 ), fabric ); // faded terracotta
|
|
1148
|
+
const drape = ( bMin, bMax, gate ) => {
|
|
1149
|
+
|
|
1150
|
+
const h = boxHit( bMin, bMax );
|
|
1151
|
+
const pleat = fabric.mul( mix( float( 0.78 ), float( 1.12 ), fract( h.p.x.mul( 2.5 ) ) ) ); // soft vertical pleats
|
|
1152
|
+
consider( h.hit.and( gate ), h.tN, pleat.mul( falloffAt( h.p.z ) ), 0.2 ); // a drape only transmits a little of the room's glow, never out-glowing the interior
|
|
1153
|
+
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
const cz0 = setback, cz1 = setback.add( 0.12 );
|
|
1157
|
+
// drape widths, biased narrow ( squared ) and each capped at half the room width, so
|
|
1158
|
+
// the two sides only meet — fully curtaining the window — in the rare room where both
|
|
1159
|
+
// are nearly closed; most rooms read partly open
|
|
1160
|
+
const sL = smoothstep( 0.3, 1.0, seed ), sR = smoothstep( 0.3, 1.0, seed2 );
|
|
1161
|
+
const lw = halfU.mul( sL.mul( sL ) ); // left drape width ( 0 below seed 0.3 )
|
|
1162
|
+
const rw = halfU.mul( sR.mul( sR ) ); // right drape width
|
|
1163
|
+
drape( vec3( halfU.negate(), floorY, cz0 ), vec3( halfU.negate().add( lw ), ceilY, cz1 ), lw.greaterThan( 0.05 ) );
|
|
1164
|
+
drape( vec3( halfU.sub( rw ), floorY, cz0 ), vec3( halfU, ceilY, cz1 ), rw.greaterThan( 0.05 ) );
|
|
1165
|
+
|
|
1166
|
+
// lit rooms read brighter and take on their bulb's colour ( the lights are on )
|
|
1167
|
+
const warmth = mix( vec3( 1.0, 1.0, 1.0 ), lightCol, lit.mul( 0.85 ) );
|
|
1168
|
+
return vec4( bestCol.mul( warmth ).mul( mix( float( 1.0 ), float( 1.3 ), lit ) ), lit.mul( bestEmit ) );
|
|
1169
|
+
|
|
1170
|
+
} );
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* The NYC masonry palette every tower is dressed from ( hex colours ): limestone-dominant
|
|
1174
|
+
* with terracotta accents. Shared by the single-tower example and {@link CityGenerator}'s
|
|
1175
|
+
* building material so both stay in sync.
|
|
1176
|
+
*/
|
|
1177
|
+
const buildingPalette = [
|
|
1178
|
+
0xa8553c, 0x9c4a34, // terracotta & red brick ( occasional accent )
|
|
1179
|
+
0x8a6a52, 0x7d6450, // warm brick / brownstone ( muted )
|
|
1180
|
+
0xc4a370, 0xb89a6f, 0xc2b183, // buff / tan
|
|
1181
|
+
0xc6c0b2, 0xc6c0b2, 0xbdb7a8, 0xd1ccbe, 0xb4afa1, // limestone / pale dressed stone — the common default
|
|
1182
|
+
0x9a988f, 0x8b8983, 0xa5a39a, // grey granite / concrete
|
|
1183
|
+
0xdbd6cb, // pale glazed ( accent )
|
|
1184
|
+
0x7c868d // steel / glass ( cool accent )
|
|
1185
|
+
];
|
|
1186
|
+
|
|
1187
|
+
/** Picks one {@link buildingPalette} colour ( a hex number ) for a tower from its seed. */
|
|
1188
|
+
function pickBuildingColor( seed ) {
|
|
1189
|
+
|
|
1190
|
+
const h = Math.abs( Math.sin( seed * 12.9898 ) * 43758.5453 );
|
|
1191
|
+
return buildingPalette[ Math.floor( ( h - Math.floor( h ) ) * buildingPalette.length ) ];
|
|
1192
|
+
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* The facade material: a single MeshStandardNodeMaterial that reads the baked
|
|
1197
|
+
* per-vertex `partId` and reproduces every zone — procedural terracotta brickwork
|
|
1198
|
+
* on the walls and piers, smooth dressed stone on the window frames and ornament,
|
|
1199
|
+
* dark glazing, and grey AC units — all dressed with world-space
|
|
1200
|
+
* weathering. One material covers the whole building ( and a whole city ), which is
|
|
1201
|
+
* what makes it compute-rasterizer friendly. `buildingBase` is the tower's flat
|
|
1202
|
+
* masonry colour as a TSL node: pass a `uniform( Color )` for a single tower, or a
|
|
1203
|
+
* per-fragment palette pick for a city, so the same material dresses both.
|
|
1204
|
+
*/
|
|
1205
|
+
function createSkyscraperMaterial( buildingBase = color( 0xc6c0b2 ) ) {
|
|
1206
|
+
|
|
1207
|
+
const soot = color( 0x4a4236 );
|
|
1208
|
+
|
|
1209
|
+
// broad weathering, all driven from world position so it reads consistently
|
|
1210
|
+
// across instanced and merged meshes: a slow tonal drift, a fine clay mottle,
|
|
1211
|
+
// and sooty vertical streaks that pool low down
|
|
1212
|
+
|
|
1213
|
+
const tone = mx_fractal_noise_float( positionWorld.mul( 0.03 ), 2 ).mul( 0.18 );
|
|
1214
|
+
const mottle = mx_noise_float( positionWorld.mul( 0.7 ) ).mul( 0.06 );
|
|
1215
|
+
const streak = mx_fractal_noise_float( vec3( positionWorld.x.mul( 1.5 ), positionWorld.y.mul( 0.04 ), positionWorld.z.mul( 1.5 ) ), 2 );
|
|
1216
|
+
const dirt = smoothstep( - 0.1, 0.45, streak ).mul( smoothstep( 210, 0, positionWorld.y ) ).mul( 0.6 );
|
|
1217
|
+
|
|
1218
|
+
// procedural terracotta brickwork in running bond, keyed off the BUILDING-LOCAL position
|
|
1219
|
+
// so the coursing anchors to each tower ( courses from its base, columns at its faces )
|
|
1220
|
+
// and lines up with the brick-snapped floor / bay dimensions. courses run up local Y;
|
|
1221
|
+
// the across-face axis is world XZ projected onto the face tangent, so brick width stays
|
|
1222
|
+
// constant on every face including the 45° chamfer. the geometry ( pre-bump ) normal is
|
|
1223
|
+
// used for the bond axis — otherwise colorNode pulls normal computation into its partId
|
|
1224
|
+
// branch and glass loses its env reflection.
|
|
1225
|
+
|
|
1226
|
+
const brickH = BRICK.height;
|
|
1227
|
+
const brickL = BRICK.length;
|
|
1228
|
+
const mortar = 0.025; // joint width, in metres
|
|
1229
|
+
|
|
1230
|
+
const nrm = normalWorldGeometry.abs();
|
|
1231
|
+
const across = positionLocal.x.mul( normalWorldGeometry.z ).sub( positionLocal.z.mul( normalWorldGeometry.x ) );
|
|
1232
|
+
const rowCoord = positionLocal.y.div( brickH );
|
|
1233
|
+
const courseRow = floor( rowCoord );
|
|
1234
|
+
const colCoord = across.div( brickL ).add( mod( courseRow, 2 ).mul( 0.5 ) ); // half-brick stagger per row
|
|
1235
|
+
|
|
1236
|
+
// anti-aliased mortar ( the "pristine grid" trick ): the drawn joint never falls below
|
|
1237
|
+
// the pixel footprint and its opacity fades to keep energy constant, so lines stay crisp
|
|
1238
|
+
// up close and dissolve far away instead of shimmering. the horizontal derivative comes
|
|
1239
|
+
// from continuous world X / Z ( weighted by the normal ), not fwidth( across ) which
|
|
1240
|
+
// would spike where the normal flips at pier edges.
|
|
1241
|
+
const mU = mortar / ( 2 * brickL );
|
|
1242
|
+
const mV = mortar / ( 2 * brickH );
|
|
1243
|
+
const ddU = nrm.z.mul( fwidth( positionWorld.x ) ).add( nrm.x.mul( fwidth( positionWorld.z ) ) ).div( brickL ).clamp( 1e-6, 0.5 );
|
|
1244
|
+
const ddV = fwidth( rowCoord ).clamp( 1e-6, 0.5 );
|
|
1245
|
+
const distU = float( 0.5 ).sub( fract( colCoord ).sub( 0.5 ).abs() );
|
|
1246
|
+
const distV = float( 0.5 ).sub( fract( rowCoord ).sub( 0.5 ).abs() );
|
|
1247
|
+
const drawU = ddU.max( mU );
|
|
1248
|
+
const drawV = ddV.max( mV );
|
|
1249
|
+
const lineU = smoothstep( drawU.add( ddU ), drawU.sub( ddU ), distU ).mul( float( mU ).div( drawU ).min( 1 ) );
|
|
1250
|
+
const lineV = smoothstep( drawV.add( ddV ), drawV.sub( ddV ), distV ).mul( float( mV ).div( drawV ).min( 1 ) );
|
|
1251
|
+
const wallFacing = smoothstep( 0.7, 0.45, nrm.y ); // brick only on vertical walls — not roofs, ledges, cornice tops
|
|
1252
|
+
const joint = lineU.max( lineV ).mul( wallFacing );
|
|
1253
|
+
|
|
1254
|
+
const brickKey = uint( courseRow.add( 1 << 16 ) ).mul( uint( 73856093 ) ).bitXor( uint( floor( colCoord ).add( 1 << 16 ) ).mul( uint( 19349663 ) ) ).toVar();
|
|
1255
|
+
const brickRnd = ihash( brickKey );
|
|
1256
|
+
const brickRnd2 = ihash( brickKey.add( uint( 1 ) ) ); // independent per-brick hash for hue
|
|
1257
|
+
|
|
1258
|
+
// soft brick relief for the bump: each brick is a gently domed mound falling to the
|
|
1259
|
+
// recessed mortar over a bevel ( distU / distV are the distance to the nearest column /
|
|
1260
|
+
// course line, 0 at the joint, 0.5 at the centre ), so bricks read rounded rather than
|
|
1261
|
+
// scratched. the bevel is widened to at least a screen pixel ( from the world-position
|
|
1262
|
+
// derivative, our stand-in for a mip LOD ) so the edge never goes sub-pixel and shimmers.
|
|
1263
|
+
const bevel = 0.02;
|
|
1264
|
+
const texel = fwidth( positionWorld ).length(); // on-screen size of a surface pixel — our hand-rolled LOD
|
|
1265
|
+
const lodBevel = texel.mul( 1.5 ).max( bevel );
|
|
1266
|
+
const brickFace = smoothstep( 0, lodBevel, distU.mul( brickL ) ).mul( smoothstep( 0, lodBevel, distV.mul( brickH ) ) ).mul( wallFacing );
|
|
1267
|
+
const reliefHeight = brickFace.mul( 0.008 );
|
|
1268
|
+
const rough = mx_noise_float( positionWorld.mul( 0.5 ) ).mul( 0.08 ).add( 0.82 ).add( joint.mul( 0.12 ) );
|
|
1269
|
+
|
|
1270
|
+
// the merged geometry carries a per-vertex partId; this material reads it and
|
|
1271
|
+
// branches to reproduce each zone — no per-part materials, compute-raster friendly
|
|
1272
|
+
|
|
1273
|
+
const partId = varying( attribute( 'partId', 'float' ) ).setInterpolation( InterpolationSamplingType.FLAT, InterpolationSamplingMode.EITHER ); // flat: a per-face id must not interpolate, or equal() below misses on the rounding
|
|
1274
|
+
const isGlass = partId.equal( GLASS );
|
|
1275
|
+
const isFrame = partId.equal( FRAME );
|
|
1276
|
+
const isOrnament = partId.equal( ORNAMENT );
|
|
1277
|
+
const isAC = partId.equal( AC );
|
|
1278
|
+
|
|
1279
|
+
// stone zones: brick + weathering on the building's colour, lightened for
|
|
1280
|
+
// piers / ornament and darkened for window frames
|
|
1281
|
+
const lighten = select( partId.equal( PIER ), float( 0.12 ), select( isOrnament, float( 0.2 ), float( 0 ) ) );
|
|
1282
|
+
const perBrick = float( 1 ).add( tone ).add( mottle ).add( brickRnd.sub( 0.5 ).mul( 0.14 ) );
|
|
1283
|
+
// per-brick warm/cool shift ( red up / blue down, or vice-versa ) so individual
|
|
1284
|
+
// bricks read as slightly different fired tones, relative to the building's colour
|
|
1285
|
+
const warmCool = brickRnd2.sub( 0.5 ).mul( 0.14 );
|
|
1286
|
+
const brickShift = vec3( float( 1 ).add( warmCool ), float( 1 ), float( 1 ).sub( warmCool ) );
|
|
1287
|
+
const tint = mix( buildingBase, color( 0xffffff ), lighten ).mul( perBrick ).mul( brickShift );
|
|
1288
|
+
const masonry = mix( tint, tint.mul( 0.6 ), joint ); // recessed joints read darker
|
|
1289
|
+
// roofs / ledges show every blotch ( flat & light ), so horizontal surfaces get a gentler,
|
|
1290
|
+
// larger-scale grime instead of the wall's streaky soot — confined to those surfaces by a
|
|
1291
|
+
// branch ( roofMask > 0 ), so the fractal never runs on the vertical facade
|
|
1292
|
+
const roofMask = wallFacing.oneMinus();
|
|
1293
|
+
const roofGrime = select( roofMask.greaterThan( 0 ), smoothstep( 0.0, 0.55, mx_fractal_noise_float( positionWorld.mul( 0.025 ), 3 ) ).mul( 0.22 ), float( 0 ) );
|
|
1294
|
+
const stoneColor = mix( masonry, soot, mix( dirt, roofGrime, roofMask ) );
|
|
1295
|
+
|
|
1296
|
+
// glass: the interior-mapped room is the base colour; the smooth, low-roughness
|
|
1297
|
+
// surface still lets a faint sky reflection ride over it, and lit rooms glow ( emissive ).
|
|
1298
|
+
// toVar so the raymarch runs once, shared by the colour and emissive outputs
|
|
1299
|
+
const room = interior().toVar();
|
|
1300
|
+
|
|
1301
|
+
// grimy glazing: the room shows through, but muted by a dusty film and dirt pooled
|
|
1302
|
+
// along the bottom of each pane, plus a baseline haze, so the panes read as old
|
|
1303
|
+
// glass rather than open holes. the streaks run down the facade ( world Y barely
|
|
1304
|
+
// scaled ); the pooled dirt uses the pane's own UV ( y = 0 at the sill ).
|
|
1305
|
+
const filmNoise = mx_fractal_noise_float( vec3( positionWorld.x.mul( 1.3 ), positionWorld.y.mul( 0.06 ), positionWorld.z.mul( 1.3 ) ), 2 );
|
|
1306
|
+
const dustStreak = smoothstep( - 0.15, 0.5, filmNoise ).mul( 0.45 );
|
|
1307
|
+
const pooled = smoothstep( 0.32, 0.0, uv().y ).mul( 0.4 );
|
|
1308
|
+
const grime = float( 0.64 ).add( dustStreak ).add( pooled ).clamp( 0, 0.95 ); // baseline haze so the panes read as dirty glass, not open holes
|
|
1309
|
+
const dirtyGlass = mix( color( 0x13161a ), color( 0x232b31 ), mx_noise_float( positionWorld.mul( 0.3 ) ).mul( 0.5 ).add( 0.5 ) );
|
|
1310
|
+
const glassColor = mix( room.xyz.mul( color( 0xb6c6bf ) ), dirtyGlass, grime ); // faint green-grey ( soda-lime ) room tint, dirtied toward grimy glass
|
|
1311
|
+
|
|
1312
|
+
// window frames are smooth dressed stone, not brick
|
|
1313
|
+
const frameColor = buildingBase.mul( 0.55 );
|
|
1314
|
+
|
|
1315
|
+
// finials / ornament: smooth dressed stone ( lightened ), not brick
|
|
1316
|
+
const ornamentColor = mix( buildingBase, color( 0xffffff ), 0.22 ).mul( float( 1 ).add( tone ) );
|
|
1317
|
+
// window AC units: a louvered white-plastic box, grimier toward the base where it drips.
|
|
1318
|
+
// keyed off the box's own UVs ( acUv.y runs 0 → 1 up each vented side )
|
|
1319
|
+
const acUv = uv();
|
|
1320
|
+
const acVent = smoothstep( 0.65, 0.4, normalWorldGeometry.y.abs() ); // 1 on the vertical vented sides, 0 on the flat top
|
|
1321
|
+
const acDetail = smoothstep( 0.08, 0.015, texel ); // louvers fade out before a slat nears a pixel
|
|
1322
|
+
const acLouver = acVent.mul( acDetail );
|
|
1323
|
+
|
|
1324
|
+
// plastic shell: off-white, some units dingier / yellowed than others
|
|
1325
|
+
const acDinge = mx_noise_float( positionWorld.mul( 0.4 ) ).mul( 0.5 ).add( 0.5 ); // ~per-unit
|
|
1326
|
+
const acPaint = mix( color( 0xf2f1ec ), color( 0xcfccc2 ), acDinge ) // bright white → light dingy grey, both lighter than the wall
|
|
1327
|
+
.add( mx_noise_float( positionWorld.mul( 5 ) ).mul( 0.04 ) );
|
|
1328
|
+
|
|
1329
|
+
// a darker recessed grille panel inset into the lighter cabinet, with horizontal louvers
|
|
1330
|
+
// inside it ( the front vents ) — the white plastic reads as a thin border frame
|
|
1331
|
+
const acGrille = smoothstep( 0.06, 0.14, acUv.x ).mul( smoothstep( 0.94, 0.86, acUv.x ) )
|
|
1332
|
+
.mul( smoothstep( 0.12, 0.2, acUv.y ) ).mul( smoothstep( 0.96, 0.88, acUv.y ) ).mul( acLouver );
|
|
1333
|
+
const acSlats = fract( acUv.y.mul( 6 ) ); // bold louvers — reads at the unit's small on-screen size
|
|
1334
|
+
const acFin = mix( float( 0.82 ), float( 1.04 ), acSlats );
|
|
1335
|
+
const acBody = acPaint.mul( mix( float( 1 ), acFin.mul( 0.42 ), acGrille ) ); // cabinet stays light; recessed grille goes dark grey
|
|
1336
|
+
|
|
1337
|
+
// grey-brown condensate grime streaking the lower edge ( plastic doesn't rust ); dirtier units streak more
|
|
1338
|
+
const acStreak = mx_fractal_noise_float( vec3( positionWorld.x.mul( 6 ), positionWorld.y.mul( 0.5 ), positionWorld.z.mul( 6 ) ), 3 ).mul( 0.5 ).add( 0.5 );
|
|
1339
|
+
const acGrime = smoothstep( 0.4, 0.0, acUv.y ).mul( acStreak ).mul( acDinge.add( 0.3 ) );
|
|
1340
|
+
const acColor = mix( acBody, color( 0x6f685a ), acGrime.mul( 0.5 ) );
|
|
1341
|
+
|
|
1342
|
+
// recessed grille ( louver fins ) relief and a slightly rougher base
|
|
1343
|
+
const acRelief = acGrille.mul( acSlats.mul( 0.012 ).sub( 0.01 ) );
|
|
1344
|
+
const acRough = float( 0.52 ).add( acGrille.mul( 0.08 ) );
|
|
1345
|
+
|
|
1346
|
+
const material = new MeshStandardNodeMaterial();
|
|
1347
|
+
material.colorNode = select( isGlass, glassColor, select( isFrame, frameColor, select( isOrnament, ornamentColor, select( isAC, acColor, stoneColor ) ) ) );
|
|
1348
|
+
material.roughnessNode = select( isGlass, float( 0.18 ), select( isOrnament, float( 0.8 ), select( isAC, acRough, rough ) ) ); // glass kept smooth for a sky reflection, but soft enough not to alias over the interior
|
|
1349
|
+
material.metalnessNode = float( 0 ); // all dielectric — stone, glass and the plastic AC shells
|
|
1350
|
+
material.emissiveNode = select( isGlass, room.xyz.mul( room.w ).mul( 4 ).mul( grime.mul( 0.6 ).oneMinus() ), color( 0x000000 ) ); // room.w = emissive weight ( 0 unlit, < 1 behind curtains ), muted further by grime
|
|
1351
|
+
material.normalNode = bumpNormal( select( isGlass.or( isFrame ).or( isOrnament ), float( 0 ), select( isAC, acRelief, reliefHeight ) ) ); // glass / frames / ornament stay flat; AC has its own louvers
|
|
1352
|
+
|
|
1353
|
+
return material;
|
|
1354
|
+
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
export { SkyscraperGenerator, createSkyscraperMaterial, buildingPalette, pickBuildingColor };
|