mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-06-06 08:38:30 +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