From 333db7fefcd72810539cf7759d9b53fe78e00909 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Thu, 27 Nov 2025 11:06:08 +0900 Subject: [PATCH] wiki update --- Glossary.mediawiki | 7 - Lighting-Engine.md | 703 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 703 insertions(+), 7 deletions(-) delete mode 100644 Glossary.mediawiki create mode 100644 Lighting-Engine.md diff --git a/Glossary.mediawiki b/Glossary.mediawiki deleted file mode 100644 index bcac94b..0000000 --- a/Glossary.mediawiki +++ /dev/null @@ -1,7 +0,0 @@ -* '''Actor''' is an entity on the world, they can be static & invisible & run script, or be a NPC. Game Player is also an extension of Actor. -* '''Blocks''' are a mininal functional division of a Blocks Spritesheet. Whereas Tiles are the smallest division, that is (usually) 16x16 portion of a Spritesheet. Blocks are a set of 1-16 Tiles. Individual Blocks can be addressed with their IDs, but Tiles can’t. 4k Spritesheets, in default configuration of 16 x 16 Tiles size, contains 4 096 Blocks and 65 536 Tiles. Tiles are one or more deterministic variants of the Block, usually different orientation/connections. Sometimes, especially in the source code, Blocks and Tiles are interchangeable. -* '''Inventory''' is an implementation of '''[https://github.com/curioustorvald/Terrarum/blob/master/src/net/torvald/terrarum/modulebasegame/gameactors/Pocketed.kt pocket]''' of an NPC. -* '''Spritesheet''' is the Spritesheet that you might know already, a flat and human-readable collection of Tiles. Maximum recommended Spritesheet size is 4 096 x 4 096 pixels; some hardware fails to utilise 8k texture. -* '''Tiles''' see Blocks -* '''World''' is an virtual system where all the virtual living things prosper, virtual soils and waters are, virtual things happen, scripts that are triggered upon certain condition. -* '''ID''' is an unique number given to Actor/Item/Faction/Possibly more to identify them. The number follows strict [[Developer:ID Generation|ID Generation Policy]]. diff --git a/Lighting-Engine.md b/Lighting-Engine.md new file mode 100644 index 0000000..465cd89 --- /dev/null +++ b/Lighting-Engine.md @@ -0,0 +1,703 @@ +# Lighting Engine + +**Audience:** Engine maintainers and module developers working with lighting systems. + +Terrarum features a sophisticated real-time lighting engine with RGB+UV colour channels, light propagation, transmittance, and global illumination. This guide covers the lighting system architecture, algorithms, and implementation. + +## Overview + +The lighting engine provides: +- **RGB+UV colour channels** — Four-channel lighting (Red, Green, Blue, UV) +- **Dynamic light propagation** — Real-time spreading across the world +- **Per-channel transmittance** — Light absorption by blocks and fluids +- **Global illumination** — Reflected light from surfaces +- **Multiple light sources** — Sun, blocks, actors, items, fluids +- **Corner occlusion** — Visual effect via simple shader +- **HDR ready** — High dynamic range lighting +- **Performant** — Optimised for large visible areas + +## Architecture + +### Cvec (Colour Vector) + +The lighting system uses `Cvec` — a four-component floating-point colour vector: + +```kotlin +class Cvec( + var r: Float = 0f, // Red channel (0.0-1.0) + var g: Float = 0f, // Green channel (0.0-1.0) + var b: Float = 0f, // Blue channel (0.0-1.0) + var a: Float = 0f // UV/Alpha channel (0.0-1.0) +) +``` + +**The fourth channel (A/W) represents UV light:** +- Used for fluorescent effects +- Affects glow sprites +- Independent from RGB channels + +### Lightmap Structure + +The lighting engine maintains a lightmap covering the visible area: + +```kotlin +object LightmapRenderer { + private var lightmap = UnsafeCvecArray(LIGHTMAP_WIDTH, LIGHTMAP_HEIGHT) + private var _mapLightLevelThis = UnsafeCvecArray(LIGHTMAP_WIDTH, LIGHTMAP_HEIGHT) + private var _mapThisTileOpacity = UnsafeCvecArray(LIGHTMAP_WIDTH, LIGHTMAP_HEIGHT) + private var _mapThisTileOpacity2 = UnsafeCvecArray(LIGHTMAP_WIDTH, LIGHTMAP_HEIGHT) +} +``` + +**Lightmap properties:** +- Dynamic size based on screen resolution and zoom +- Includes overscan region for smooth light propagation +- Stored in unsafe memory arrays for performance +- Updated every frame + +### Coordinate Systems + +```kotlin +// World coordinates → Array coordinates +private inline fun Int.convX() = this - for_x_start + overscan_open +private inline fun Int.convY() = this - for_y_start + overscan_open + +// Check if world coordinate is in lightmap bounds +private fun inBounds(x: Int, y: Int) = + (y - for_y_start + overscan_open in 0 until LIGHTMAP_HEIGHT && + x - for_x_start + overscan_open in 0 until LIGHTMAP_WIDTH) +``` + +## Light Sources + +### 1. Sunlight (Global Light) + +The world has a global ambient light level: + +```kotlin +val sunLight = world.globalLight // Cvec +``` + +**Sunlight properties:** +- Fills all open-air tiles +- Penetrates through non-solid blocks +- Blocked by solid terrain and walls +- Changes with time of day and weather + +**Sunlight application:** + +```kotlin +// Open air || luminous tile backed by sunlight +if ((!terrainProp.isSolid && !wallProp.isSolid) || + (tileLuminosity.nonZero() && !wallProp.isSolid)) { + lightLevel.set(sunLight) +} +``` + +### 2. Block Luminosity + +Blocks can emit light: + +```kotlin +class BlockProp { + internal var baseLumColR = 0f + internal var baseLumColG = 0f + internal var baseLumColB = 0f + internal var baseLumColA = 0f + + fun getLumCol(x: Int, y: Int): Cvec +} +``` + +**Example luminous blocks:** +- Lava: `(0.7664, 0.2032, 0.0, 0.0)` — Orange-red +- Glowstone: `(0.8, 0.8, 0.6, 0.0)` — Warm yellow +- Torches: Dynamic flicker via `dynamicLuminosityFunction` + +**Dynamic luminosity:** + +Blocks with `dynamicLuminosityFunction > 0` have animated lighting: + +```kotlin +var dynamicLuminosityFunction: Int = 0 // 0=static, 1-7=various patterns +``` + +The engine pre-generates 64 virtual tiles per dynamic light block with randomised variations, creating effects such as per-tile randomised flickering. + +### 3. Fluid Luminosity + +Fluids can emit light based on fluid amount: + +```kotlin +if (fluid.type != Fluid.NULL) { + val fluidAmount = fluid.amount.coerceIn(0f, 1f).pow(0.5f) + tileLuminosity.maxAndAssign(fluidProp.lumCol.mul(fluidAmount)) +} +``` + +**Fluid light properties:** +- Scales with fluid level (square root for better visual) +- Blends with terrain luminosity (max of both) +- Full block of lava = full brightness +- Partial block = reduced brightness + +### 4. Actor Lights + +Actors can have lightboxes: + +```kotlin +class ActorWithBody { + val lightBoxList: MutableList // Emitted light + val shadeBoxList: MutableList // Shadows cast +} + +data class Lightbox(val box: Hitbox, val colour: Cvec) +``` + +**Actor light sources:** +- Defined via lightboxes attached to actor +- Move with actor position +- Scale with actor scale +- Multiple lightboxes per actor supported + +**Example: Glowing actor** + +```kotlin +actor.lightBoxList.add(Lightbox( + Hitbox(0.0, 0.0, 32.0, 32.0), + Cvec(0.8f, 0.4f, 0.1f, 0.0f) // Orange glow +)) +``` + +### 5. Item Lights + +Held items emit light: + +```kotlin +// Held item automatically creates lightbox +val heldItem = actor.inventory.itemEquipped[GameItem.EquipPosition.HAND_GRIP] +val light = ItemCodex[heldItem]?.getLumCol() ?: Cvec(0) +``` + +Items define luminosity via: + +```kotlin +abstract class GameItem { + open fun getLumCol(): Cvec = Cvec(0) +} +``` + +## Light Propagation Algorithm + +### The "Light Swiper" + +The engine uses a multi-pass directional sweeping algorithm: + +```kotlin +// Pass 1: Precalculation of tile luminosity/opacity before swiping +precalculate(lightmap, x, y) + +// Pass 2: Four directional sweeps +r1(lightmap) // Horizontal ↔ +r2(lightmap) // Vertical ↕ +r3(lightmap) // Diagonal ⤡ +r4(lightmap) // Diagonal ⤢ + +// Pass 3: Precalculation of tile reflectance before secondary swiping +precalculate2(lightmap, x, y) + +// Pass 4: Second sweep pass +r1(lightmap); r2(lightmap); r3(lightmap); r4(lightmap) + +// Optional additional passes for quality +for (pass in 3..lightpasses) { + r1(lightmap); r2(lightmap); r3(lightmap); r4(lightmap) +} +``` + +**Algorithm complexity:** O(8n) per pass, where n is lightmap size + +### Directional Sweeps + +Each sweep propagates light in one direction: + +#### Horizontal Sweep (r1) + +```kotlin +fun r1(lightmap: UnsafeCvecArray) { + for (line in 1 until LIGHTMAP_HEIGHT - 1) { + swipeLight( + 1, line, // Start: left edge + LIGHTMAP_WIDTH - 2, line, // End: right edge + 1, 0, // Direction: ↔ + lightmap, false // Not diagonal + ) + } +} +``` + +Propagates light left-to-right across each row. + +#### Vertical Sweep (r2) + +```kotlin +fun r2(lightmap: UnsafeCvecArray) { + for (line in 1 until LIGHTMAP_WIDTH - 1) { + swipeLight( + line, 1, // Start: top edge + line, LIGHTMAP_HEIGHT - 2, // End: bottom edge + 0, 1, // Direction: ↕ + lightmap, false + ) + } +} +``` + +Propagates light top-to-bottom down each column. + +#### Diagonal Sweeps (r3, r4) + +```kotlin +// Diagonal ⤡ (top-left to bottom-right) +fun r3(lightmap: UnsafeCvecArray) { + for (i in 0 until LIGHTMAP_WIDTH + LIGHTMAP_HEIGHT - 5) { + swipeLight( + max(1, i - LIGHTMAP_HEIGHT + 4), + max(1, LIGHTMAP_HEIGHT - 2 - i), + min(LIGHTMAP_WIDTH - 2, i + 1), + min(LIGHTMAP_HEIGHT - 2, (LIGHTMAP_WIDTH + LIGHTMAP_HEIGHT - 5) - i), + 1, 1, // Direction: ⤡ + lightmap, true // Diagonal + ) + } +} + +// Diagonal ⤢ (bottom-left to top-right) +fun r4(lightmap: UnsafeCvecArray) { + // Similar but with direction (1, -1) +} +``` + +Propagates light diagonally across the map. + +### Bidirectional Swipe + +Each sweep runs bidirectionally: + +```kotlin +fun swipeLight(sx: Int, sy: Int, ex: Int, ey: Int, dx: Int, dy: Int, lightmap: UnsafeCvecArray, swipeDiag: Boolean) { + // Forward pass: start → end + var x = sx, y = sy + while (x*dx <= ex*dx && y*dy <= ey*dy) { + swipeTask(x, y, x-dx, y-dy, lightmap, swipeDiag) + x += dx; y += dy + } + + // Backward pass: end → start + x = ex; y = ey + while (x*dx >= sx*dx && y*dy >= sy*dy) { + swipeTask(x, y, x+dx, y+dy, lightmap, swipeDiag) + x -= dx; y -= dy + } +} +``` + +**Why bidirectional?** +- Ensures light propagates in both directions +- Eliminates directional bias +- Produces uniform light spread + +## Transmittance and Opacity + +### Per-Channel Opacity + +Blocks and fluids have opacity per colour channel: + +```kotlin +class BlockProp { + var opacity = Cvec() // Per-channel opacity (0.0-1.0) +} + +// Examples: +stoneOpacity = Cvec(0.629, 0.629, 0.629, 0.629) // Solid blocks light uniformly +waterOpacity = Cvec(0.102, 0.074, 0.051, 0.083) // Blue-tinted +``` + +**Opacity values:** +- `0.0` — Fully transparent (no light absorption) +- `0.5` — Half absorption +- `1.0` — Fully opaque (complete absorption) + +### Light Attenuation + +Light passing through a tile is attenuated per channel: + +```kotlin +fun darkenColoured(x: Int, y: Int, opacity: Cvec, lightmap: UnsafeCvecArray): Cvec { + return lightmap.getVec(x, y).lanewise { light, channel -> + light * (1f - opacity.lane(channel) * lightScalingMagic) + } +} + +private const val lightScalingMagic = 1.0f / 8.0f // Tuning constant +``` + +**Attenuation formula per channel:** + +``` +newLight = oldLight × (1 - opacity × scalingFactor) +``` + +**Exponential decay:** + +Recursive application creates exponential decay curve: +- Light halves every ~5.5 tiles through stone +- More transparent materials allow light further +- Per-channel opacity creates coloured shadows + +### Diagonal Attenuation + +Diagonal propagation uses increased opacity: + +```kotlin +val diagonalOpacity = opacity * sqrt(2) // ≈ 1.414 +``` + +**Why?** Light travels ~1.414× farther diagonally, requiring proportional attenuation. + +### Opacity Blending + +Multiple opacity sources blend: + +```kotlin +// Terrain opacity +tileOpacity.set(terrainProp.opacity) + +// Blend fluid opacity +if (fluid.type != Fluid.NULL) { + val fluidAmount = fluid.amount.coerceIn(0f, 1f).pow(0.5f) + tileOpacity.max(fluidProp.opacity.mul(fluidAmount)) +} + +// Blend shadow opacity +tileOpacity.max(shadowMap[blockAddress] ?: colourNull) +``` + +Uses `max()` to blend — most opaque channel wins. + +## Global Illumination + +### Reflectance + +Blocks reflect nearby light based on surface colour and reflectance: + +```kotlin +fun precalculate2(lightmap: UnsafeCvecArray, x: Int, y: Int) { + // Blend nearby 4 lights to get ambient intensity + ambientLight.set(0) + .maxAndAssign(lightmap.getVec(x - 1, y)) + .maxAndAssign(lightmap.getVec(x + 1, y)) + .maxAndAssign(lightmap.getVec(x, y - 1)) + .maxAndAssign(lightmap.getVec(x, y + 1)) + + // Calculate reflected colour + val tileColour = App.tileMaker.terrainTileColourMap[terrainProp.id] + val reflectedLight = tileColour + .mul(terrainProp.reflectance) + .mul(giScale) // 0.35 global scaling + + // Add to tile light + lightLevel.max(reflectedLight) +} +``` + +**Reflectance properties:** +- `0.0` — Fully absorbing (no reflection) +- `0.5` — Moderate reflection +- `1.0` — Fully reflective (mirror-like) + +**Common reflectance values:** +- Stone: 0.0-0.3 (low) +- Metal: 0.7-0.9 (high) +- Mirrors: 1.0 (perfect) + +### Global Illumination Scale + +```kotlin +private const val giScale = 0.35f +``` + +Reduces reflected light intensity to prevent over-brightening. + +## Shadow System + +Actors can cast shadows: + +```kotlin +data class Shadebox(val box: Hitbox, val opacity: Cvec) + +actor.shadeBoxList.add(Shadebox( + Hitbox(0.0, 0.0, 32.0, 32.0), + Cvec(0.5f, 0.5f, 0.5f, 0.0f) // 50% shadow +)) +``` + +**Shadow blending:** + +```kotlin +// Shadows blend with tile opacity +tileOpacity.max(shadowMap[blockAddress] ?: colourNull) +``` + +Shadows increase tile opacity, darkening areas. + +## Corner Occlusion + +**Note:** Corner occlusion is a visual effect implemented via a simple shader, not part of the lighting calculation. + +The shader darkens tile corners based on adjacent tile solidity, creating the illusion of ambient occlusion without expensive ray tracing. + +## Performance Optimisations + +### 1. Unsafe Memory Arrays + +```kotlin +class UnsafeCvecArray(width: Int, height: Int) { + private val ptr: Long = UnsafeHelper.allocate(width * height * 16) // 4 floats × 4 bytes +} +``` + +**Benefits:** +- Direct memory access +- No GC pressure +- Cache-friendly layout +- Great performance improvement + +### 2. Overscan Region + +```kotlin +const val overscan_open: Int = 40 +const val overscan_opaque: Int = 10 +``` + +Lightmap extends beyond visible area: +- Prevents edge artefacts +- Allows smooth light propagation +- Different sizes for open vs opaque areas + +### 3. Pre-Calculation + +```kotlin +fun precalculate(x: Int, y: Int) { + // Cache tile properties + _thisTerrain = world.getTileFromTerrainRaw(x, y) + _thisTerrainProp = BlockCodex[_thisTerrain] + + // Cache opacity + _mapThisTileOpacity.setVec(x, y, _thisTerrainProp.opacity) + + // Cache luminosity + _thisTileLuminosity.set(_thisTerrainProp.getLumCol(x, y)) +} +``` + +Avoids repeated property lookups during sweeps. + +### 4. Static Local Variables + +```kotlin +// Reused across function calls to avoid allocations +private val sunLight = Cvec(0) +private val _ambientAccumulator = Cvec(0) +private val _reflectanceAccumulator = Cvec(0) +private val _thisTileOpacity = Cvec(0) +``` + +Eliminates per-frame allocations. + +### 5. Multi-Pass Tuning + +```kotlin +// Configurable pass count +val passcnt = App.getConfigInt("lightpasses") // Default: 2-3 +``` + +**Trade-offs:** +- 2 passes: ~4-6 ms, minor dark spots +- 3 passes: ~6-9 ms, good quality +- 4+ passes: ~8-12+ ms, excellent quality + +### Performance Metrics + +**Typical frame timings (Intel 6700K CPU):** +- Lantern/shadow map build: ~0.003 ms +- Precalculation 1: ~1-2 ms +- Light pass 1: ~2-3 ms +- Precalculation 2: ~1-2 ms +- Light pass 2: ~2-3 ms +- **Total:** ~6-10 ms per frame + +**Optimisation history:** +- Original: ~15-20 ms +- Direct memory access: ~12-15 ms +- NEWLIGHT2 algorithm: ~6-10 ms +- Current: ~4-8 ms (with tuning) + +## Technical Details + +### Lightmap Sizing + +```kotlin +private var LIGHTMAP_WIDTH: Int = + (1f / ZOOM_MINIMUM) * screenWidth / TILE_SIZE + overscan_open * 2 + 3 + +private var LIGHTMAP_HEIGHT: Int = + (1f / ZOOM_MINIMUM) * screenHeight / TILE_SIZE + overscan_open * 2 + 3 +``` + +**Size factors:** +- Screen resolution +- Minimum zoom level (for worst-case sizing) +- Overscan region +- Extra padding for edge cases + +### Light Map Rendering + +```kotlin +fun getLight(x: Int, y: Int): Cvec? { + // Handle world wrapping (round world on X-axis) + val wrappedX = if (/* wrap condition */) x + world.width + else if (/* other wrap */) x - world.width + else x + + if (!inBounds(wrappedX, y)) return null + + return lightmap.getVec(wrappedX.convX(), y.convY()) +} +``` + +Handles: +- World wrapping on X-axis (round world) +- Out-of-bounds queries +- Coordinate conversion + +### Lantern and Shadow Maps + +```kotlin +private val lanternMap = HashMap() +private val shadowMap = HashMap() +``` + +**Purpose:** +- Store actor light/shadow contributions +- Keyed by block address for fast lookup +- Cleared and rebuilt each frame + +**Building process:** + +```kotlin +fun buildLanternAndShadowMap(actorContainer: List) { + lanternMap.clear() + shadowMap.clear() + + actorContainer.forEach { actor -> + actor.lightBoxList.forEach { (box, colour) -> + // Rasterise lightbox to affected blocks + forEachTileInBox(box) { blockAddr -> + lanternMap[blockAddr] = (lanternMap[blockAddr] ?: Cvec(0)) + .maxAndAssign(colour) + } + } + // Similar for shadeBoxList → shadowMap + } +} +``` + +## Creating Light Sources + +### Luminous Block + +Define block with light emission: + +```csv +"id";"lumr";"lumg";"lumb";"lumuv";"dlfn" +"200";"0.8";"0.6";"0.2";"0.0";"0" +``` + +Static orange glow. + +### Dynamic Light Block + +```csv +"id";"lumr";"lumg";"lumb";"lumuv";"dlfn" +"201";"0.9";"0.4";"0.0";"0.0";"2" +``` + +Flickering torch (dlfn=2). + +### Glowing Item + +```kotlin +class GlowingOrb(originalID: ItemID) : GameItem(originalID) { + override fun getLumCol(): Cvec { + return Cvec(0.3f, 0.8f, 0.9f, 0.2f) // Cyan with UV + } +} +``` + +### Actor with Light + +```kotlin +class Firefly(referenceID: ActorID) : ActorWithBody(referenceID) { + init { + lightBoxList.add(Lightbox( + Hitbox(8.0, 8.0, 16.0, 16.0), // Centre glow + Cvec(0.9f, 0.9f, 0.3f, 0.0f) // Yellow + )) + } +} +``` + +### Coloured Opacity + +Create coloured glass: + +```csv +"id";"shdr";"shdg";"shdb";"shduv";"solid" +"300";"0.8";"0.2";"0.2";"0.0";"0" +``` + +Red-tinted glass (high red shade, low GB). + +## Best Practises + +1. **Use appropriate opacity** — Match material properties +2. **Balance luminosity** — Avoid over-bright sources +3. **Leverage UV channel** — Create fluorescent effects +4. **Test dynamic lights** — Ensure flickering looks good +5. **Tune reflectance** — Metals high, rock low +6. **Consider performance** — Limit excessive light sources +7. **Use per-channel opacity** — Create coloured shadows +8. **Test in darkness** — Verify light propagation +9. **Blend fluids properly** — Scale with fluid amount +10. **Profile lighting passes** — Monitor frame time + +## Common Pitfalls + +- **Too many light sources** — Performance degrades +- **Opacity = 1.0 everywhere** — Light can't propagate +- **No reflectance** — World looks flat +- **Ignoring UV channel** — Missing fluorescent opportunities +- **Uniform opacity** — No coloured shadows +- **Excessive luminosity** — Blindingly bright +- **Dynamic lights everywhere** — Flickering overload +- **Not testing zoom** — Lightmap size issues +- **Forgetting shadows** — Actors float unnaturally +- **Wrong light scaling** — Dark spots or over-bright areas + +## See Also + +- [[Blocks#Luminosity]] — Block light properties +- [[Actors]] — Actor lightboxes and shadeboxes +- [[World]] — Global light and day/night cycle +- [[Weather]] — Weather lighting effects