wiki update

minjaesong
2025-11-27 11:06:08 +09:00
parent 3019ace6dd
commit 333db7fefc
2 changed files with 703 additions and 7 deletions

@@ -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 cant. 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]].

703
Lighting-Engine.md Normal file

@@ -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<Lightbox> // Emitted light
val shadeBoxList: MutableList<Shadebox> // 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<BlockAddress, Cvec>()
private val shadowMap = HashMap<BlockAddress, Cvec>()
```
**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<ActorWithBody>) {
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