name: tinyworld-render-performance description: Use when changing Tiny World Builder renderer setup, shadows, smoke, voxel clouds, ghost board render cost, frame loop, or GPU performance.
Tiny World Render Performance
Keep the renderer single-pass and predictable.
Current renderer contract:
- Default render path is single-pass:
renderer.render(scene, camera)straight to the canvas. The only sanctioned post-process is the optional pixelation pass (low-res render target + depth/normal-edge fullscreen quad) gated behind thePixel size/Pixel depth edge/Pixel normal edgerender settings. WhenrenderPixelSize <= 1or XR is presenting, that pass MUST bypass and fall back to direct rendering — do not introduce other always-on post passes (EffectComposer, screen shaders, additional render targets) without explicit approval. - Cap DPR; do not return to uncapped
devicePixelRatio. - Main WebGL context uses
antialias: true; the old smoothing/post pass has been removed. - Brightness/saturation/contrast are lightweight CSS filters on the WebGL canvas, not shader uniforms.
GPU caches (introduced for low-end GPU + visible-distance scaling):
geomCachememoizesroundedSlab/roundedBoxExtrudeGeometries by their numeric args. Geometries are taggeduserData.cached = trueand shared across every mesh that asks for the same shape. Disposal goes throughsafeDisposeGeometry(geo)— never callgeo.dispose()directly on these. If you add a new geometry helper that's called more than a handful of times, cache it the same way.getOpenBoxGeometry(w, h, d, skipTop, skipBottom, skipPX, skipNX, skipPZ, skipNZ)returns a cachedBoxGeometrywith selected face groups removed from the index buffer (matIdx 2 = top, matIdx 3 = bottom). Use it for risers, terrain caps, and hidden-face optimizations. Terrain caps should remain clean flat box slabs rather thanroundedSlabbevels: flat top surfaces plus vertical cap thickness provide depth/substance without the repeated chevron artifacts caused by chamfered per-tile bevels. Grass caps should overhang the dirt body so exposed edges read as green lip over dirt. Open-box geometries are stilluserData.cached = true— never mutate or dispose.vbox()acceptsskipTop/skipBottom/ side-skip options and routes those pieces through cached open-box geometry. Use these flags for buried voxel faces in authored assemblies, especially floating-island underside slabs and inverted roof layers; do not render closed boxes for the inside of the island mass.- Voxel terrain panels must stay batched inside each tile: bucket panels by cached open-box geometry and material, then emit one
THREE.InstancedMeshper bucket. Strip panel bottoms, internal side faces, and neighbour-hidden edge sides withgetOpenBoxGeometry; do not go back to oneTHREE.Meshper small terrain panel at 8x8/12x12 resolutions. - Object-free, level-1 grass tiles use the simple flat-grass fast path even when voxel terrain is enabled. A blank island must not spend hundreds of draw calls on per-cell voxel grass panels/details; reserve voxel terrain detail for edited, raised, non-grass, or occupied cells.
- Simple blank-grass terrain is receive-only for shadows. Do not put empty grass slabs into the shadow-map pass; they receive object shadows but should not double the blank-island draw call count.
- Preview/ghost board rendering is forced off by default: Preview distance/window/opacities persist as zero and the Settings controls are removed. Do not let hidden Preview settings create ghost boards or fade traversal on a blank island.
- When voxel terrain is enabled, keep the top surface voxelized but render exposed riser/body sides as solid shader-textured walls. Avoid thousands of side panels: they create cracks/transparent-looking corners and waste draw/instance budget. Keep same-or-higher neighbour side culling intact so shared internal sides are not rendered.
- Pixelation shader AA should work in pixel mode, but only through edge/depth/normal detection. Do not use a broad fullscreen blur; it smears terrain texture and UI-like decals. Shader AA must not force the normal prepass by itself; only
Pixel normal edgeshould allocate/render the normal target. - Pixel post shaders must preserve the renderer output encoding. In Three r128
ShaderMaterialalready injects encoding helpers, so include/applyencodings_fragmentat the finalgl_FragColorstep but do not duplicateencodings_pars_fragment. - Backdrop/game-screen vignette should remain a cheap CSS overlay variable, not another WebGL post pass. Keep it separate from scene brightness/lighting so it can frame the background without retuning materials.
- Sky/background colour controls are direct scene/CSS settings, not post passes:
Sky blue depthdarkens the shader sphere and CSS backdrop,Sky blue saturationpushes the same blue hue harder, andUndercloud widthrebuilds the small under-island cloud ring.Cloud heightalso controls undercloud depth below the island, slightly farther than the upper cloud distance, so one height adjustment moves both cloud layers. Keep the undercloud layer as a handful of instanced cloud-puff groups attached below the floating island; do not make a full volumetric cloud field or reuse the full multi-mesh shadow-casting sky cloud factory there. - Night star backgrounds use the single
star-vault-equirect-spheremesh and a procedural star-only canvas texture, blended only at dusk/night throughrenderStarVaultStrength. Keep it as a non-depth-writing sky sphere, not an EffectComposer/background pass. Do not load painted sky/land/cloud bitmaps into this sphere: equirect wrapping makes horizon imagery smear under/around the island. Keep the shader horizon mask so stars fade out below the island. - Floating-island underside depth should use a small number of cached voxel/box slabs and cached utility cylinders attached to
homeBorderGroup, not per-cell underside geometry. Treat underside/edge/rocket/utility dressing as decorative scenery: setcastShadow = falseandreceiveShadow = falseafter building it so hundreds of tiny underside meshes do not enter the shadow-map pass. - Floating-island shell sides must stay visible from underside/grazing angles, including secondary editable islands. Keep island underside materials double-sided, keep proxy side shells on double-sided cached material clones, and keep the persistent recessed side-backing ring behind edge greebles on home and duplicate island bases. Let the scene-level island culling hide/show the shell group; do not rely on per-mesh frustum culling for the shell slabs because it can clip side/back faces while the island itself is visible.
- Animated waterfall froth/chunks should stay batched as
THREE.InstancedMeshpools per exposed water edge, not one mesh per puff/drop. Keep the instance matrix update insideupdateWaterfallEffects()and mark the pools non-shadowing. - Terrain/path storytelling details should be batched or rare. Wheel ruts,
surface flecks, pavers, pebbles, grass-edge roots, and small path signs/crates
belong in the voxel terrain surface-detail pass with cached geometries,
InstancedMeshbuckets where possible, andnoShadowon decorative extras; do not scatter dozens of shadow-casting meshes per tile. - Waterfall curtain/blade variety should come from the shared shader sheets
(
getWaterfallCurtainMaterial()/getWaterfallSurfaceMaterial()), so each exposed water edge uses one vertical sheet plus one surface-flow sheet instead of many blade/tail meshes. If a future pass merges whole rows of waterfalls, keep the same shader-sheet contract and aggregate spans above the per-cell tile factories. - Voxel object factories that emit repeated
vbox()pieces should finish withoptimizeVoxelObjectGroup(...)so matching geometry/material pieces become localTHREE.InstancedMeshbuckets. Keep the legacy factory code intact; the optimization is a render-layer replacement for repeated live meshes, not a removal of the authored object. customPartssphere/ellipsoid primitives use the sharedgetCustomPartEllipsoidGeometry(...)cache and per-mesh scale. Do not create a freshSphereGeometryfor every generated rounded part; repeated domes, balloons, canopies, and tanks should share cached VBOs and be skipped bysafeDisposeGeometry.- Async texture/resource callbacks must repaint through
renderSceneIfReady(), not directrenderScene()calls. These callbacks can fire while classic scripts are still loading;bootApp()flipssetRenderSceneReady(true)only after scene setup is complete and before the animation loop starts. - Home and duplicate floating-island bases count as voxel object assemblies too:
run
optimizeVoxelObjectGroup(...)after underside/engine/edge dressing is authored so repeated clamps, boxes, and small slabs batch before LOD copies multiply their draw cost. - After batching a floating-island base, run
mergeStaticBaseMeshesByMaterial(...)for the fixed shell/underside/greeble meshes. Keep animated plumes, propellers, weather, waterfalls, and selectable editable-island engines out of that merge. - Distant mini-world dressing is background scenery: merge it by material after build and keep it non-shadowing/non-pickable. It must not add hundreds of shadow-casting meshes to a blank board.
- Contiguous same-style voxel fence rows should render as span meshes through
findFenceRenderSpan()/makeVoxelFenceSpan()while keeping per-cellworldintent and tile picking unchanged. Refresh the full connected fence component after edits so old anchors/non-anchors do not linger. - Waterfall froth/foam should drift slowly. Keep
WATERFALL_FROTH_SPEEDconservative (currently0.30) so the white puff layer reads as moving foam, not flashing particles. - The object/stamp voxel bevel is a persisted render setting (
tinyworld:render:voxelBevel) applied insidevbox()through cached centered voxel box geometry. It is intentionally fine-grained (0.001 steps) so tiny voxels can keep only a slight softened edge. Keep it subtle and global; do not hand-bevel individual stamps unless they need a genuinely different silhouette. - Voxel terrain top panels need a small width/depth overlap. Exact edge-to-edge panels produce sub-pixel cracks in the pixelated render path, especially on dark soil. Do not add a full top underlay to hide seams: terrain tile fade materials are transparent/depthWrite-off, so broad underlays can sort over the voxel panels and make the surface read flat.
- Terrain leak blockers belong under the dirt/riser body as bottom caps, not beneath the visible top surface. Mark those caps
userData.noShadow = trueand keepcastReceive()respecting that flag so they block sky/background misses without adding shadow cost or flattening voxel tops. - Terrain/island side and riser meshes must not receive projected shadows. Tag side-only risers/kerbs/rims with
userData.noReceiveShadow = trueand keepgroundReceiveOnly()respecting it; otherwise sun shadows from top content draw large diagonal bands on vertical side materials that appear to rotate while orbiting the camera. - Far zoom/VOD also disables terrain shadow receivers via
updateTerrainShadowReceiversForCamera()so cap/top meshes that necessarily combine top + vertical side faces cannot show distant diagonal shadow bands. Preserve the near/far cutoff as a zoom-aware terrain-only shadow LOD; object shadows can read close-up, but distant terrain sides should stay clean. fadeMatCacheshares fade materials inFADE_BUCKETS = 16opacity buckets keyed by (base material UUID, grayscale flag, bucket, keepFadeAtOpaque).prepareFadeableandapplyElementOpacitylook up viapickFadeMaterial(baseMat, grayscale, displayOpacity, keepFadeAtOpaque)instead of cloning per mesh. Terrain tile roots setkeepFadeAtOpaqueso they remain on the transparent/depthWrite-off fade material even at 100%; snapping terrain back to the base opaque material exposes diagonal face artifacts that are absent at 99% opacity. Cached materials are taggeduserData.cachedFade = trueand must never be mutated or disposed — they're shared by every mesh in their bucket. If you need a per-instance opacity (e.g. squash anim), clone the material yourself and tag it so it gets disposed individually.- Ghost boards are built incrementally via
pendingGhostBoardsqueue, drained insideanimate()byprocessGhostBoardQueue(budgetMs)with a small per-frame budget.ensureGhostBoardsAroundTargetonly enqueues — it must never build synchronously, or load/reset/visible-distance changes hitch the main thread. - Per-frame object work is set-based:
animatedCellObjectstracks swaying trees/tufts/crops andsmokeHouseObjectstracks chimney sources. Do not return to scanning everycellMeshesentry each frame for these effects. - Pixel-drag panning must not call
ensureGhostBoardsAroundTarget()directly on every pointer event. Route panning throughmaybeEnsureGhostBoardsAroundTarget()so preview-board enqueue/fade work only runs after a meaningful target move or a board-coordinate change; settings/reset/import paths may still force an immediate ensure when they deliberately change preview state. - Ghost detail reevaluation is dormant by default because Preview boards are
currently full-fidelity only. Keep
ghostDetailReevaluationActivefalse unless a non-full-detail board exists; otherwise the animation loop should not scan every ghost board several times per second just to confirm'full' === 'full'. applyElementOpacity()caches the last display opacity applied to each root. Preserve that no-op guard so repeated fade/bubble updates do not traverse an unchanged tile/object subtree or redo fade-material bucket checks.- Per-object appearance texture overrides are relative multipliers on top of
the base material's
userData.worldTextureScale, not absolute world scales. KeepcustomTextureMaterial()multiplying by the base material scale so roof shingles/slate and brick courses do not balloon when selected through the inspector/appearance path. applyWorldUVs()patches Lambert materials throughonBeforeCompile, so it must also assign a matchingcustomProgramCacheKey()(scale + seam/edge options). Any clone/fade/shaded/shell material that copies a world-UVonBeforeCompilehook must copycustomProgramCacheKeytoo; otherwise Three r128 may reuse a stock/local-UV program and side textures appear to rotate when the camera orbits.- Generated/imported world application supports sliced progressive rendering. In sliced mode,
applyState(..., { sliced: true })sorts terrain and object/detail passes by distance fromopts.renderOriginor the current cameratarget, so visible/nearby cells appear before farther cells. Preserve that distance-ranked ordering when changing generation rendering. Demo/stress routes may passskipGhostBoards: trueto keep a large home board from also preloading preview boards; in that modeapplyStateshould zero the in-memory preview distance, sync the ghost budget, and clear ghost boards without persisting render settings. - Stamps panel card thumbnails share the toolbar thumbnail renderer/cache but should not synchronously build every 3D thumbnail during open/search/category renders. Keep fallback thumbs immediate, cancel stale card-thumb queues on panel re-render, and drain expensive card thumbs in small
requestAnimationFramebatches. - Home grids above the windowing threshold are intent-full / render-windowed:
world[][]may hold the full 512×512 board, butcellMeshesmust only hold the camera-centred home render window. Keep large-grid bulk load/clear paths on intent writes plusrequestHomeRenderWindowSync(), notGRID²mesh rebuilds. Keepworld[][]sparse: virtual default grass comes fromgetWorldCell()/ensureWorldCell(), not from preallocatingHOME_GRID_MAX²cells. Any directworld[x][z]read on an editing/API path must either guard the row or usegetWorldCell()so untouched large-grid rows still behave as default terrain. - Duplicate editable islands follow the same sparse-intent rule: a new island gets one pickable default grass surface and only edited cells get materialized through
setCell(). Do not restore per-islandGRID²grass seeding; 50 islands must stay mostly proxies/sparse cells until edited. - Use
?demo=island-stress&islands=50&stats=1orwindow.__runIslandStressDemo(50)to measure duplicate-island scaling. The stats overlay reports island LOD counts (full/proxy/hidden) plus the active full-detail budget and livecellMeshes. Keep duplicate editable islands under a capped full-detail LOD budget; the selected island remains full and only the nearest in-budget islands keep full bases/engines/content, while the rest use proxies. - Preview/ghost boards are full
GRID²boards today. Until they are chunked/windowed too, clamp 96+ grids toghostRadius = 0/ preview distance 0, and keep 128+ boards preview-disabled. Otherwise a single neighbour at 128+ explodes into tens of thousands of meshes/instances per board. Do not degrade visible Preview objects into cheap proxy boxes/cones/pyramids; if full-fidelity preview is too expensive, reduce or disable preview rings instead. If the cheap ghost terrain instancing path is used, clear its global buckets when ghost boards are cleared/disabled/resized so stale instanced terrain cannot remain in the scene. - To keep draw calls low on full-detail ghost boards, all terrain tiles and objects are merged by material and fade role (
'tile'/'object') into single meshes usingmergeGhostTerrainByMaterial(board). The merged meshes are centered at the board center to preserve distance-based fading viaopacityAtWorldPosition. Raycasting resolves click/hover cell coordinates(gx, gz)usingresolveRaycastCell(h)by mapping hit coordinates relative to the board bounds. When a ghost cell is materialized (clicked or edited),removeGhostCellMeshtriggersrebuildGhostBoardto regenerate the merged meshes without the edited cell. - Full-quality ghost tiles are
THREE.Grouproots with many static leaf meshes.mergeGhostTerrainByMaterial(board)must traverse those leaf meshes, merge by each leaf's base material, and skip special animated/effect meshes such as waterfall/weather children. Do not regress to only checking direct board children; that leaves the merge path effectively disabled. - The final generated/imported settle pass should only rebuild adjacency-sensitive terrain (paths, water/shore neighbours, bridges), not the entire board.
- Initial/full-scene render paths should render board tiles immediately and animate only props/buildings/extras. Do not reintroduce tile drop-in for the starter board, saved-state restore, import, or generated-world base pass; the terrain is the stage, not part of the entrance animation.
- If grass/water/path show tiny specks, pavers, ripples, or foam before the
rest of a tile visually settles, inspect
makeTile()decals and the reveal pipeline first: grass flecks, water insets/ripples/foam, and path pavers/scuffs are real geometry just above the tile top. During opacity reveal they can read as transient artifacts because faded materials use transparent/depthWrite-off buckets, and sliced builds may briefly render adjacency-sensitive path/water/shore details before the final settle pass. - Stats overlay (
?stats=1or backtick key) readsrenderer.infoand reports FPS, draws, tris, geoms, mats, programs, textures, ghost-board count + queue depth. It also shows the repaint profiler when visible: render submit buckets, frame tick buckets,setCellrefresh/plan/save time, tile/object/extras rebuild time, queue drains, and dispose traversal. Use?repaint=1orwindow.__tinyworldRepaintProfile.setEnabled(true)for a focused repaint breakdown, andwindow.__tinyworldRepaintProfile.snapshot()to inspect the current top buckets. - Camera culling runs immediately before
renderer.render()inupdateSceneVisibilityForCamera(). Keep this scene-level pass in addition to meshfrustumCulled: it hides off-frustum home/ghost/editable-island roots before the camera and shadow passes. When the camera moves below an island, terrain tile roots must stay visible so the side walls remain behind underside greebles; fade only top-side object/extras withrenderCullOpacity, and mask that transition with a short event-driven 2Dunder-occlusion-cloud-wipesweep. The cloud wipe must not stay as a persistent full-screen fog layer while the camera rests in the transition band. The stats overlayculledrow is the quick sanity check that draw/tris totals are responding to what is actually visible. - Current default color grade is intentionally stylized: resolution 75%, brightness 80%, saturation 109%, contrast 120%, lighting 50%, ambient fill 100%, front/side/back fill 10%, tilt blur 10.5px, and tilt focus 21%.
- Render settings are user-adjustable and persisted in
localStorageundertinyworld:render:*. - Tilt-shift blur stays active while the camera is moving, panning, zooming,
home-tweening, or first-person walking/look-moving. Keep
markCameraMoving()as a stable no-op hook for those movement paths, but do not hide or pause the tilt-shift pseudo-element during interaction unless the user explicitly asks. - Scene/screen controls must keep working in the direct-render path: resolution, shadow quality, lighting, visible distance, visible size, backdrop glow, clouds, tilt-shift blur/focus, and ghost opacity.
- Preview window is the reveal square around the camera target in tile-width units. It auto-scales by board size and can be user-adjusted, but it must never be smaller than
GRID. Do not subtract half a tile from this radius, or the board edge starts fading inside the requested size. - Preview opacity / floors / objects are user-adjustable display multipliers for surrounding preview boards. The home board stays fully opaque regardless of those controls.
- Do not add new post-only shader controls beyond the existing pixelation/antialias controls unless the user explicitly asks for them. Pixelation/AA post-process targets/materials are constructed lazily inside
ensurePixelResourcesand resize throughsetSize; do not pre-allocate them at startup, and do not leak the normal-target/override material whenrenderPixelNormalEdgeis 0. Depth/normal edge strengths should default to 0: they outline real tile bevels, risers, overhangs, shore/path/water decals, and shadowed side geometry, which can read as terrain artifacts under pixelation. Shader antialias is a colour-only edge-aware smoothing pass; keep it separate from depth/normal outlines. - Soft perspective has been removed from the app. Keep camera mode choices to top-down, isometric/orthographic, perspective, and first-person; normalize any legacy
cameraMode: "soft"import/save data toperspective. - Atmosphere fade should use native
scene.fog(THREE.Fog) so distant scenery colour-fades inside the direct renderer path. Keep fog near/far recomputed from camera distance + visible span after camera updates, expose it as a persisted render setting, and disable it whenscene.background === nullfor AR passthrough. The fog colour should be derived from the live sky/background but blended heavily toward a warm neutral haze, not the raw saturated sky blue, so distant islands do not wash out cyan. - The Generate modal's landscape "Realistic" option no longer uses the LandscapeEngine continuous mesh — it now builds flat-top voxel blocks through the mesh-terrain system (
applyRealisticVoxelLandscape()in27-landscape-engine.js→window.__tinyworldMeshTerrain.generate(...), sampling the samesampleLandscapeCell()height/biome). Realistic keepslandscapeMeshMode = false(normal tiles + voxel overlay). Low-poly still uses the LandscapeEngine continuous mesh (landscapeMeshMode = trueviainitLandscapeMesh()), and planet-underlay still uses its realistic Lambert path. See.codex/skills/tinyworld-mesh-terrain. - LandscapeEngine realistic terrain in TinyWorld may use built-in vertex-colour Lambert materials so native shadow maps and
scene.fogwork, but do not replace low-poly LandscapeEngine terrain with Lambert. Low-poly landscape must keep its custom celsandMatLowPolyshader; otherwise the low-poly render option visually regresses into realistic terrain. Keep near realistic terrain receiving shadows and near rocks/flora casting shadows; far LOD terrain should stay non-shadow-receiving/casting to avoid wasting GPU. For planet-underlay realistic terrain, keep the Lambert path but inject only the smallsetPlanetFog()/terrainMat.onBeforeCompileunderlay haze uniforms; do not switch the low-poly planet to Lambert or add a global post blur.setPlanetFog()must defensively ensure those uniform holders exist before writing.value, because restore/query boot can call it before a compiled material has all underlay fields populated. - When testing a planet/ground surface below floating islands, keep it as a separate lowered LandscapeEngine instance (
planetLandscapeEngine) instead of flippinguseLandscapeEngine/landscapeMeshMode. That preserves the editable floating board and ghost-board behavior while the underlay streams independently. Treat that underlay as backdrop, not an active play surface: keep enough terrain mesh fidelity and extent for the planet to read as a broad detailed surface (near radius 0, far radius about 2 with larger far chunks, far chunk size around 2600, far res around 24), but remove the expensive active-surface costs: no rock/flora scatter, no water plane by default, no shadow participation, one near/far chunk build per throttled stream tick, and only a couple of cheap transparent atmosphere sheets. If a proof route shows a clipped/partial horizon because the normal animation loop is paused or throttled, use a tiny setTimeout warmup drain that builds one pending chunk at a time and re-renders, rather than priming dozens of chunks synchronously. Patch bothLandscapeEngine.jsand the activeengine/landscape/chunks.jsmixin when changing chunk builders; the mixin overrides_makeChunk()at runtime. Planet distance is user-adjustable through the Generate modal / queryplanetDrop; changing it should move the lowered LandscapeEngine group and rescale the between-layer atmosphere sheets, not alter the floating board height. Add the island-to-planet atmosphere as cheap transparent world-space haze sheets between the board and underlay (planetAtmosphereGroup), not as a global post blur; depth testing keeps the editable island crisp while softening only the lower landscape behind it. The underlay should read as far below, not as wallpaper behind the island: use the planet distance uniforms (planetDistanceEffect, tint colour, desaturation, dimming) on low-poly/realistic/water shaders and tint/fade built-in rock/flora materials instead of adding a full-screen blur or global post pass. Mark non-editable underlay roots withuserData.noPointerPickand exclude them frompickTile()raycast roots; otherwise every mouse move raycasts through all lowered terrain chunks/flora and causes visible app stalls. Do not setmaterial.needsUpdate = trueon recurring haze colour syncs unless transparency/depth-write mode actually changed. - Shadow maps should stay modest unless a visual defect proves otherwise.
- Keep shadow bias/normalBias/radius tight for voxel-scale geometry. Large
normalBiasor soft radius values make thin roofs, columns, crop stems, fences, and trim detach from their shadows, which reads as light leaking through the model in pixel mode. Do not forcematerial.shadowSide = THREE.FrontSideglobally: closed box roofs and voxel panels will self-shadow and show diagonal shadow-acne hatching that looks like an unwanted texture. - Rain/snow should use in-world instanced box particles. Rain impacts use transient instanced ring-ripple splash pools plus heavy-rain/storm circular puddle buildup; snow impacts add persistent low-opacity square surface patches that visually build up. Snow is winter-only: selecting snow switches to winter, and changing to any non-winter season clears snow weather. Keep impact decals lifted above beveled tile tops (
WEATHER_SURFACE_PAD+ decal/ripple lift), but leave depth testing enabled so they cannot render through terrain sides, objects, or underside geometry. Do not reintroduce CSS/screen-space rain/snow overlays or always-on per-tile weather panels. Impacts should only appear on rendered tile surfaces. Weather state should affect every visible element through shared material tinting, including preview boards. Weather intensity is severity: low = light rain/flurries, high = storms/snowstorms with stronger slant, darker ambience, more active instances, global material tint strength, and water/snow buildup. Intensity and splash/buildup controls intentionally overdrive up to 300%; keep emission/opacity visibly obvious at max. Storm is an explicit rain mode that forces storm-strength rain visuals while preserving the same splash/buildup controls. Seed surface marks when weather or splash/intensity changes so puddles/snow are visible immediately, not only after waiting for random impacts. Clamp impact decals inside their tile footprint so rings/puddles/snow patches never overhang visible board edges. - The sun is the only shadow caster. Its angle is fixed in world space
(
SUN_OFFSET = (7, 12, 5)) but its position andsun.targetfollow the cameratargetviaupdateSunFollow()(called fromupdateCamera()). The shadow frustum is±SHADOW_HALF (20)in light space so shadows stay correct wherever the user pans — never anchor the sun at the world origin again. - Lighting stack:
AmbientLight(flat fill so shadowed sides never go black) +HemisphereLight(warm sky/ground gradient) + the directional sun + non-shadowing front/side/back directional fill lights. Keep sun/shadow strength separate from fill controls so dark object faces can be lifted without increasing cast-shadow cost. Keep neutral/default lighting conservative now that there is no post pass; time-of-day hemisphere scaling should normalize against the day anchor (0.90), not the raw constructor value, or midday blows out. - Imported GLB/model stamps also have named non-shadowing safety lights:
model-stamp-import-ambient-fillandmodel-stamp-import-directional-fill. They are adapted from the user-supplied Mugen87 StackOverflow ambient/directional baseline, scaled by the render lighting/ambient sliders and time-of-day/weather code, and must follow the camera target without casting shadows. - Accent lighting for the starlit look is limited to non-shadowing local
SpotLight/PointLightinstances that follow the camera target and fade in at dusk/night. Do not enable shadow maps on these lights; the directional sun remains the only shadow caster. - Placeable lamp/spotlight stamps may include capped non-shadowing
PointLight/SpotLightsources, but the visible falloff should be cheap: shared additive ground decals, wall/halo sprites, and haze meshes taggeduserData.lightVisualsoprepareFadeable()does not replace their shader materials. Keep the spotlight cone/decal source at the fixture and widen/fade it forward onto the ground, not back toward the object. - Building windows can switch to
M.windowLitat dusk/night via per-window deterministic seeds. Keep this set-based (buildingWindowObjects) and update on time-of-day changes, not by scanning every cell each frame. Window spill/wall glow/halo helpers should be taggeduserData.windowLightEffectand skipped by fade-material traversal so their additive shader materials survive ghost/reveal opacity setup. - Building light readability depends on the exterior halo and ground spill. Keep
the window halo bloom broad enough to read from normal editor distance
(
windowW * 2.2,windowH * 2.0increateWindowLightEffects()), and do not reintroduce an unmasked wall-surface glow plane that paints light through surrounding brickwork. - The blast shield is the supplied
VoxelShieldclass/API port inengine/world/40-shield-system.js, not a replacement design. PreserveVoxelKit,BlastPanel,CornerKeystone,ShieldRing,ShieldDemo, andwindow.VoxelShield. Keep panel/keystone voxel pieces optimized withoptimizeVoxelObjectGroup(...), and cap actualPointLights while retaining emissive rune/glow cubes. - Ghost boards should participate in the shadow pass — same sun, same shadows everywhere. If Preview/ghost shadows disappear, first check that
prepareFadeablehas not forced ghost meshes tocastShadow = false, and that any merged/batched ghost terrain explicitly preservesreceiveShadow/castShadowafter replacing source meshes. The factory-levelcastReceive/groundReceiveOnlychoices should apply uniformly unless there is a deliberate, visible-quality-approved LOD exception. - Voxel cloud visual opacity is independent from Cloud shadow. Do not drive visible cloud materials with
alphaTest; cloud shadow breakup belongs on each puff'scustomDepthMaterialso lowering the shadow slider never hides the clouds themselves. - Cloud rim lighting is material/shader tint only: voxel cloud material clones
use a small warm emissive, while soft cloud shader quads use a
rimStrengthuniform. Keep rim lighting separate fromrenderCloudShadowso cloud shadow cost/visibility rules do not change. - Sky clouds should stay above and around the active build plane, not directly
over rooftops/buildings in overhead editor views. Keep the high visual minimum
and no-fly/perimeter exclusion in
23-particles-clouds.jsand31-cloud-sea.jsaligned with the Cloud height settings/defaults so clouds do not read as textures mapped onto buildings. - When Cloud shadow is 0, cloud puffs should set
castShadow = falseso they leave the shadow-map pass entirely. Alpha-testing every cloud out in the depth material still costs draw calls. - Cloud shadow defaults to 0. Visible clouds may remain, but they must not enter the shadow pass unless the user explicitly raises Cloud shadow.
- Smoke particles must be capped and must not cast/receive shadows.
- Per-particle opacity should use the shared quantized particle material cache and skip material assignment when the quantized bucket has not changed. Do not clone or assign particle materials every frame for smoke/dust.
- Ambient underside wear/debris should use a separate capped particle pool with cached tiny box geometry and the shared quantized particle material cache, so slow falling crumbs do not consume chimney/impact smoke capacity. Make them visible by tuning cap/rate/size within that pool before adding another effect path. Object landing bursts should also spawn through that same pool at the object's x/z instead of creating a second underside particle system.
- Floating-island rocket/engine smoke puffs should reuse the existing capped
smokeParticlespool and cached particle materials. Impact-triggered puffs from heavy drop-ins should be brief, grey/dark-grey, non-shadowing particles rather than a second smoke system. - Home-island rocket/jet plumes should be a few yaw-facing shader sheets using cached plane geometry and shared materials, not dozens of individual animated flame cubes per engine. Rotate the sheet only around the local Y axis so it reads from the camera without copying the full camera quaternion or moving the thrust off the nozzle. The tick path should update one shared time uniform plus light sheet scale/position pulses, while intermittent smoke stays on the capped particle pool. Keep the old voxel plume object builder available as an inactive legacy helper so those objects can be reused later without re-authoring them.
- Duplicate-island lift propellers should switch to one cached circle geometry plus one shared dark shader material for the high-RPM blur/strobe disc. Keep the physical blade groups visible only during startup/slow spin, keep bulky outer caps and old hub-block cubes opt-in rather than default, and avoid per-prop material clones or per-frame shader allocation.
- Under-island clouds and rocket plume/smoke effects must render before board/tile transparent fade materials (
UNDER_ISLAND_EFFECT_RENDER_ORDER) so foreground grass, cliffs, fences, and buildings visually occlude them instead of sorting behind the puffs. - Crop duster planes are behind the persisted
tinyworld:render:planesEnabledswitch and default off during the current performance pass. When off, do not load the GLB/textures, tick propellers, update plane banners, or leave crop dust particles alive. When on, planes remain ambient year-round: only crop-dusting passes are summer/crop-gated; non-summer or no-crop states should fall back to banner flyovers rather than hiding the plane system. - Ghost board frustum culling: In
renderScene(), active ghost boards must be dynamically frustum-culled using the camera view frustum. Apply a safety padding (e.g.,GRID * TILE * 0.5) to the bounding boxes to prevent mountain shadow pop-out. - Cheap ghost terrain bounds culling: Set
frustumCulled = trueon the instanced meshes of cheap ghost terrain. Update their geometry bounding boxes and spheres inupdateGhostRenderBubble()to match the active preload area so they are culled as a single unit when the camera is panned away. - Landscape chunk frustum culling: Position chunk groups at their world coordinates and place their child terrain mesh and instanced rocks/flora at local coordinates relative to the chunk center. Shared geometries (
rockGeo,pineGeo, etc.) must have pre-calculated local bounding boxes spanning the chunk size so Three.js can correctly transform their bounds and frustum-cull instanced meshes.
Validation:
- Run the inline script syntax check.
- Open
http://localhost:3000/tiny-world-builder. - Confirm
renderer.getPixelRatio()is at or below the cap. - Confirm there are no
postTarget/postMaterial/postProcessingEnabledreferences intiny-world-builder.html. - Confirm no console errors after reload.
Under-occlusion top-content fade + cloud wipe
When the camera dips below an island surface, top content (cells/objects) fades
out then stops rendering, masked by a sweeping cloud. Tuning lives in
01-render-core.js:
- Fade window:
renderCullTopContentOpacity=renderCullSmoothstep(-2.85, 0.55, camera.y - surfaceWorldY). Wider window = more gradual fade before the cull (opacity ~0 →setRenderCullVisible(false)). Keep the upper edge just above the surface and the lower edge a few units down. - Cloud wipe (
updateUnderOcclusionCloudWipe, element#under-occlusion-cloud-wipe): triggers on a one-shot phase crossing (prevPhase<0.42 && nextPhase>=0.42down />0.58 -> <=0.58up). Opacity cap ~`0.72, decaydt * 1.15(lower = lingers longer, to cover the longer fade). The CSS band is ~80vh tall so it covers most of the viewport as it sweeps. Verify the time-based envelope with real frame dt — a synthetic instant loop can't rampunderOcclusionWipeActive`.