mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
wiki update
@@ -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]].
|
||||
703
Lighting-Engine.md
Normal file
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
|
||||
Reference in New Issue
Block a user