From 747e3183d667393f0977559dd3b1f03be84437b4 Mon Sep 17 00:00:00 2001 From: UnknownShadow200 Date: Sat, 22 Apr 2017 15:03:06 +1000 Subject: [PATCH] Modularise lighting more --- ClassicalSharp/ClassicalSharp.csproj | 1 + ClassicalSharp/Game/Game.cs | 9 +- .../Map/Lighting/BasicLighting.Updater.cs | 130 ++++++++++++++++++ ClassicalSharp/Map/Lighting/BasicLighting.cs | 42 +----- ClassicalSharp/Map/Lighting/IWorldLighting.cs | 11 +- ClassicalSharp/Map/World.cs | 2 +- ClassicalSharp/Network/Protocols/BlockDefs.cs | 4 +- ClassicalSharp/Rendering/ChunkUpdater.cs | 91 +----------- .../Rendering/Env/WeatherRenderer.cs | 8 +- ClassicalSharp/Rendering/MapRenderer.cs | 17 ++- 10 files changed, 165 insertions(+), 150 deletions(-) create mode 100644 ClassicalSharp/Map/Lighting/BasicLighting.Updater.cs diff --git a/ClassicalSharp/ClassicalSharp.csproj b/ClassicalSharp/ClassicalSharp.csproj index c4f225fd9..351cee072 100644 --- a/ClassicalSharp/ClassicalSharp.csproj +++ b/ClassicalSharp/ClassicalSharp.csproj @@ -187,6 +187,7 @@ + diff --git a/ClassicalSharp/Game/Game.cs b/ClassicalSharp/Game/Game.cs index 17494a9aa..fa17d52f9 100644 --- a/ClassicalSharp/Game/Game.cs +++ b/ClassicalSharp/Game/Game.cs @@ -283,17 +283,18 @@ namespace ClassicalSharp { } public void UpdateBlock(int x, int y, int z, BlockID block) { - int oldHeight = Lighting.GetLightHeight(x, z) + 1; BlockID oldBlock = World.GetBlock(x, y, z); World.SetBlock(x, y, z, block); - Lighting.UpdateLight(x, y, z, oldBlock, block); WeatherRenderer weather = WeatherRenderer; if (weather.heightmap != null && !World.IsNotLoaded) weather.OnBlockChanged(x, y, z, oldBlock, block); + Lighting.OnBlockChanged(x, y, z, oldBlock, block); - int newHeight = Lighting.GetLightHeight(x, z) + 1; - MapRenderer.RedrawBlock(x, y, z, block, oldHeight, newHeight); + // Refresh the chunk the block was located in. + int cx = x >> 4, cy = y >> 4, cz = z >> 4; + MapRenderer.GetChunk(cx, cy, cz).AllAir &= BlockInfo.Draw[block] == DrawType.Gas; + MapRenderer.RefreshChunk(cx, cy, cz); } float limitMilliseconds; diff --git a/ClassicalSharp/Map/Lighting/BasicLighting.Updater.cs b/ClassicalSharp/Map/Lighting/BasicLighting.Updater.cs new file mode 100644 index 000000000..d658bab36 --- /dev/null +++ b/ClassicalSharp/Map/Lighting/BasicLighting.Updater.cs @@ -0,0 +1,130 @@ +// Copyright 2014-2017 ClassicalSharp | Licensed under BSD-3 +using System; +using ClassicalSharp.Renderers; + +#if USE16_BIT +using BlockID = System.UInt16; +#else +using BlockID = System.Byte; +#endif + +namespace ClassicalSharp.Map { + + /// Manages lighting through a simple heightmap, where each block is either in sun or shadow. + public sealed partial class BasicLighting : IWorldLighting { + + public override void OnBlockChanged(int x, int y, int z, BlockID oldBlock, BlockID newBlock) { + bool didBlock = info.BlocksLight[oldBlock]; + bool nowBlocks = info.BlocksLight[newBlock]; + int oldOffset = (info.LightOffset[oldBlock] >> Side.Top) & 1; + int newOffset = (info.LightOffset[newBlock] >> Side.Top) & 1; + + // Two cases we need to handle here: + if (didBlock == nowBlocks) { + if (!didBlock) return; // a) both old and new block do not block light + if (oldOffset == newOffset) return; // b) both blocks blocked light at the same Y coordinate + } + + int index = (z * width) + x; + int lightH = heightmap[index]; + // Since light wasn't checked to begin with, means column never had meshes for any of its chunks built. + // So we don't need to do anything. + if (lightH == short.MaxValue) return; + + int oldHeight = lightH + 1; + if ((y - newOffset) >= lightH) { + if (nowBlocks) { + heightmap[index] = (short)(y - newOffset); + } else { + // Part of the column is now visible to light, we don't know how exactly how high it should be though. + // However, we know that if the old block was above or equal to light height, then the new light height must be <= old block.y + CalcHeightAt(x, y, z, index); + } + } else if (y == lightH && oldOffset == 0) { + // For a solid block on top of an upside down slab, they will both have the same light height. + // So we need to account for this particular case. + BlockID above = y == (height - 1) ? Block.Air : game.World.GetBlock(x, y + 1, z); + if (info.BlocksLight[above]) return; + + if (nowBlocks) { + heightmap[index] = (short)(y - newOffset); + } else { + CalcHeightAt(x, y - 1, z, index); + } + } + int newHeight = heightmap[index] + 1; + RefreshAffected(x, y, z, newBlock, oldHeight, newHeight); + } + + MapRenderer renderer; + void RefreshAffected(int x, int y, int z, BlockID block, int oldHeight, int newHeight) { + int cx = x >> 4, cy = y >> 4, cz = z >> 4; + renderer = game.MapRenderer; + + // NOTE: It's a lot faster to only update the chunks that are affected by the change in shadows, + // rather than the entire column. + int newCy = newHeight < 0 ? 0 : newHeight >> 4; + int oldCy = oldHeight < 0 ? 0 : oldHeight >> 4; + int minCy = Math.Min(oldCy, newCy), maxCy = Math.Max(oldCy, newCy); + ResetColumn(cx, cy, cz, minCy, maxCy); + World world = game.World; + + int bX = x & 0x0F, bY = y & 0x0F, bZ = z & 0x0F; + if (bX == 0 && cx > 0) + ResetNeighbour(x - 1, y, z, block, cx - 1, cy, cz, minCy, maxCy); + if (bY == 0 && cy > 0 && Needs(block, world.GetBlock(x, y - 1, z))) + renderer.RefreshChunk(cx, cy - 1, cz); + if (bZ == 0 && cz > 0) + ResetNeighbour(x, y, z - 1, block, cx, cy, cz - 1, minCy, maxCy); + + if (bX == 15 && cx < renderer.chunksX - 1) + ResetNeighbour(x + 1, y, z, block, cx + 1, cy, cz, minCy, maxCy); + if (bY == 15 && cy < renderer.chunksY - 1 && Needs(block, world.GetBlock(x, y + 1, z))) + renderer.RefreshChunk(cx, cy + 1, cz); + if (bZ == 15 && cz < renderer.chunksZ - 1) + ResetNeighbour(x, y, z + 1, block, cx, cy, cz + 1, minCy, maxCy); + } + + bool Needs(BlockID block, BlockID other) { + return info.Draw[block] != DrawType.Opaque || info.Draw[other] != DrawType.Gas; + } + + void ResetNeighbour(int x, int y, int z, BlockID block, + int cx, int cy, int cz, int minCy, int maxCy) { + World world = game.World; + if (minCy == maxCy) { + int index = x + world.Width * (z + y * world.Length); + ResetNeighourChunk(cx, cy, cz, block, y, index, y); + } else { + for (cy = maxCy; cy >= minCy; cy--) { + int maxY = Math.Min(world.Height - 1, (cy << 4) + 15); + int index = x + world.Width * (z + maxY * world.Length); + ResetNeighourChunk(cx, cy, cz, block, maxY, index, y); + } + } + } + + void ResetNeighourChunk(int cx, int cy, int cz, BlockID block, + int y, int index, int nY) { + World world = game.World; + int minY = cy << 4; + + // Update if any blocks in the chunk are affected by light change + for (; y >= minY; y--) { + BlockID other = world.blocks[index]; + bool affected = y == nY ? Needs(block, other) : info.Draw[other] != DrawType.Gas; + if (affected) { renderer.RefreshChunk(cx, cy, cz); return; } + index -= world.Width * world.Length; + } + } + + void ResetColumn(int cx, int cy, int cz, int minCy, int maxCy) { + if (minCy == maxCy) { + renderer.RefreshChunk(cx, cy, cz); + } else { + for (cy = maxCy; cy >= minCy; cy--) + renderer.RefreshChunk(cx, cy, cz); + } + } + } +} diff --git a/ClassicalSharp/Map/Lighting/BasicLighting.cs b/ClassicalSharp/Map/Lighting/BasicLighting.cs index a334ac158..9c0d8be24 100644 --- a/ClassicalSharp/Map/Lighting/BasicLighting.cs +++ b/ClassicalSharp/Map/Lighting/BasicLighting.cs @@ -80,7 +80,7 @@ namespace ClassicalSharp.Map { } } - public override int GetLightHeight(int x, int z) { + int GetLightHeight(int x, int z) { int index = (z * width) + x; int lightH = heightmap[index]; return lightH == short.MaxValue ? CalcHeightAt(x, height - 1, z, index) : lightH; @@ -126,45 +126,5 @@ namespace ClassicalSharp.Map { for (int i = 0; i < heightmap.Length; i++) heightmap[i] = short.MaxValue; } - - public override void UpdateLight(int x, int y, int z, BlockID oldBlock, BlockID newBlock) { - bool didBlock = info.BlocksLight[oldBlock]; - bool nowBlocks = info.BlocksLight[newBlock]; - int oldOffset = (info.LightOffset[oldBlock] >> Side.Top) & 1; - int newOffset = (info.LightOffset[newBlock] >> Side.Top) & 1; - - // Two cases we need to handle here: - if (didBlock == nowBlocks) { - if (!didBlock) return; // a) both old and new block do not block light - if (oldOffset == newOffset) return; // b) both blocks blocked light at the same Y coordinate - } - - int index = (z * width) + x; - int lightH = heightmap[index]; - if (lightH == short.MaxValue) { - // We have to calculate the entire column for visibility, because the old/new block info is - // useless if there is another block higher than block.y that blocks sunlight. - CalcHeightAt(x, height - 1, z, index); - } else if ((y - newOffset) >= lightH) { - if (nowBlocks) { - heightmap[index] = (short)(y - newOffset); - } else { - // Part of the column is now visible to light, we don't know how exactly how high it should be though. - // However, we know that if the old block was above or equal to light height, then the new light height must be <= old block.y - CalcHeightAt(x, y, z, index); - } - } else if (y == lightH && oldOffset == 0) { - // For a solid block on top of an upside down slab, they will both have the same light height. - // So we need to account for this particular case. - BlockID above = y == (height - 1) ? Block.Air : game.World.GetBlock(x, y + 1, z); - if (info.BlocksLight[above]) return; - - if (nowBlocks) { - heightmap[index] = (short)(y - newOffset); - } else { - CalcHeightAt(x, y - 1, z, index); - } - } - } } } diff --git a/ClassicalSharp/Map/Lighting/IWorldLighting.cs b/ClassicalSharp/Map/Lighting/IWorldLighting.cs index 10adbe700..986992c26 100644 --- a/ClassicalSharp/Map/Lighting/IWorldLighting.cs +++ b/ClassicalSharp/Map/Lighting/IWorldLighting.cs @@ -24,13 +24,10 @@ namespace ClassicalSharp.Map { // Except this function is a lot more optimised and minimises cache misses. public unsafe abstract void LightHint(int startX, int startZ, BlockID* mapPtr); - /// Returns the y coordinate of the highest block that is fully not in sunlight. - /// *** Does NOT check that the coordinates are inside the map. ***
- /// e.g. if cobblestone was at y = 5, this method would return 4.
- public abstract int GetLightHeight(int x, int z); - - /// Updates the lighting for the block at that position, which may in turn affect other blocks. - public abstract void UpdateLight(int x, int y, int z, BlockID oldBlock, BlockID newBlock); + /// Called when a block is changed, to update the lighting information. + /// Derived classes ***MUST*** mark all chunks affected by this lighting change + /// as needing to be refreshed. + public abstract void OnBlockChanged(int x, int y, int z, BlockID oldBlock, BlockID newBlock); /// Discards all cached lighting information. public virtual void Refresh() { } diff --git a/ClassicalSharp/Map/World.cs b/ClassicalSharp/Map/World.cs index 8d75e1f1c..395c3c262 100644 --- a/ClassicalSharp/Map/World.cs +++ b/ClassicalSharp/Map/World.cs @@ -71,7 +71,7 @@ namespace ClassicalSharp.Map { /// Sets the block at the given world coordinates without bounds checking, /// and also recalculates the heightmap for the given (x,z) column. public void SetBlock(Vector3I p, BlockID blockId) { - SetBlock(p.X, p.Y, p.Z, blockId); + blocks[(p.Y * Length + p.Z) * Width + p.X] = blockId; } /// Returns the block at the given world coordinates without bounds checking. diff --git a/ClassicalSharp/Network/Protocols/BlockDefs.cs b/ClassicalSharp/Network/Protocols/BlockDefs.cs index a3eb4c54a..25a68d01e 100644 --- a/ClassicalSharp/Network/Protocols/BlockDefs.cs +++ b/ClassicalSharp/Network/Protocols/BlockDefs.cs @@ -68,6 +68,7 @@ namespace ClassicalSharp.Network.Protocols { byte HandleDefineBlockCommonStart(NetReader reader, bool uniqueSideTexs) { byte id = reader.ReadUInt8(); BlockInfo info = game.BlockInfo; + bool didBlockLight = info.BlocksLight[id]; info.ResetBlockProps(id); info.Name[id] = reader.ReadString(); @@ -85,8 +86,7 @@ namespace ClassicalSharp.Network.Protocols { } info.SetTex(reader.ReadUInt8(), Side.Bottom, id); - // Need to refresh lighting when a block's light blocking state changes - bool didBlockLight = info.BlocksLight[id]; + // Need to refresh lighting when a block's light blocking state changes info.BlocksLight[id] = reader.ReadUInt8() == 0; if (!game.World.IsNotLoaded && (didBlockLight != info.BlocksLight[id])) { game.Lighting.Refresh(); diff --git a/ClassicalSharp/Rendering/ChunkUpdater.cs b/ClassicalSharp/Rendering/ChunkUpdater.cs index 61c6a62da..af0d09197 100644 --- a/ClassicalSharp/Rendering/ChunkUpdater.cs +++ b/ClassicalSharp/Rendering/ChunkUpdater.cs @@ -160,9 +160,10 @@ namespace ClassicalSharp.Renderers { width = NextMultipleOf16(game.World.Width); height = NextMultipleOf16(game.World.Height); length = NextMultipleOf16(game.World.Length); - chunksX = width >> 4; - chunksY = height >> 4; - chunksZ = length >> 4; + + chunksX = width >> 4; renderer.chunksX = chunksX; + chunksY = height >> 4; renderer.chunksY = chunksY; + chunksZ = length >> 4; renderer.chunksZ = chunksZ; int count = chunksX * chunksY * chunksZ; if (renderer.chunks == null || renderer.chunks.Length != count) { @@ -236,90 +237,6 @@ namespace ClassicalSharp.Renderers { void ContextLost() { ClearChunkCache(); } void ContextRecreated() { Refresh(); } - - public void RedrawBlock(int x, int y, int z, BlockID block, int oldHeight, int newHeight) { - int cx = x >> 4, cy = y >> 4, cz = z >> 4; - - // Does this chunk now contain air? - ChunkInfo curInfo = renderer.unsortedChunks[cx + chunksX * (cy + cz * chunksY)]; - curInfo.AllAir &= game.BlockInfo.Draw[block] == DrawType.Gas; - - // NOTE: It's a lot faster to only update the chunks that are affected by the change in shadows, - // rather than the entire column. - int newCy = newHeight < 0 ? 0 : newHeight >> 4; - int oldCy = oldHeight < 0 ? 0 : oldHeight >> 4; - int minCy = Math.Min(oldCy, newCy), maxCy = Math.Max(oldCy, newCy); - ResetColumn(cx, cy, cz, minCy, maxCy); - World world = game.World; - - int bX = x & 0x0F, bY = y & 0x0F, bZ = z & 0x0F; - if (bX == 0 && cx > 0) - ResetNeighbour(x - 1, y, z, block, cx - 1, cy, cz, minCy, maxCy); - if (bY == 0 && cy > 0 && Needs(block, world.GetBlock(x, y - 1, z))) - RefreshChunk(cx, cy - 1, cz); - if (bZ == 0 && cz > 0) - ResetNeighbour(x, y, z - 1, block, cx, cy, cz - 1, minCy, maxCy); - - if (bX == 15 && cx < chunksX - 1) - ResetNeighbour(x + 1, y, z, block, cx + 1, cy, cz, minCy, maxCy); - if (bY == 15 && cy < chunksY - 1 && Needs(block, world.GetBlock(x, y + 1, z))) - RefreshChunk(cx, cy + 1, cz); - if (bZ == 15 && cz < chunksZ - 1) - ResetNeighbour(x, y, z + 1, block, cx, cy, cz + 1, minCy, maxCy); - } - - bool Needs(BlockID block, BlockID other) { - return info.Draw[block] != DrawType.Opaque || info.Draw[other] != DrawType.Gas; - } - - void ResetNeighbour(int x, int y, int z, BlockID block, - int cx, int cy, int cz, int minCy, int maxCy) { - World world = game.World; - if (minCy == maxCy) { - int index = x + world.Width * (z + y * world.Length); - ResetNeighourChunk(cx, cy, cz, block, y, index, y); - } else { - for (cy = maxCy; cy >= minCy; cy--) { - int maxY = Math.Min(world.Height - 1, (cy << 4) + 15); - int index = x + world.Width * (z + maxY * world.Length); - ResetNeighourChunk(cx, cy, cz, block, maxY, index, y); - } - } - } - - void ResetNeighourChunk(int cx, int cy, int cz, BlockID block, - int y, int index, int nY) { - World world = game.World; - int minY = cy << 4; - - // Update if any blocks in the chunk are affected by light change - for (; y >= minY; y--) { - BlockID other = world.blocks[index]; - bool affected = y == nY ? Needs(block, other) : info.Draw[other] != DrawType.Gas; - if (affected) { RefreshChunk(cx, cy, cz); return; } - index -= world.Width * world.Length; - } - } - - void ResetColumn(int cx, int cy, int cz, int minCy, int maxCy) { - if (minCy == maxCy) { - RefreshChunk(cx, cy, cz); - } else { - for (cy = maxCy; cy >= minCy; cy--) - RefreshChunk(cx, cy, cz); - } - } - - public void RefreshChunk(int cx, int cy, int cz) { - if (cx < 0 || cy < 0 || cz < 0 || - cx >= chunksX || cy >= chunksY || cz >= chunksZ) return; - - ChunkInfo info = renderer.unsortedChunks[cx + chunksX * (cy + cz * chunksY)]; - if (info.AllAir) return; // do not recreate chunks completely air - info.Empty = false; - info.PendingDelete = true; - } - int chunksTarget = 12; const double targetTime = (1.0 / 30) + 0.01; diff --git a/ClassicalSharp/Rendering/Env/WeatherRenderer.cs b/ClassicalSharp/Rendering/Env/WeatherRenderer.cs index faf1f2750..2a3877332 100644 --- a/ClassicalSharp/Rendering/Env/WeatherRenderer.cs +++ b/ClassicalSharp/Rendering/Env/WeatherRenderer.cs @@ -191,10 +191,10 @@ namespace ClassicalSharp.Renderers { int index = (x * length) + z; int height = heightmap[index]; - // Two cases can be skipped: - // a) rain height hadn't been calculated to begin with - // b) modified is below current rain height - if (height == short.MaxValue || y < height) return; + // Two cases can be skipped here: + // a) rain height was not calculated to begin with (height is short.MaxValue) + // b) changed y is below current calculated rain height + if (y < height) return; if (nowBlock) { // Simple case: Rest of column below is now not visible to rain. diff --git a/ClassicalSharp/Rendering/MapRenderer.cs b/ClassicalSharp/Rendering/MapRenderer.cs index 9a07e6051..2b3e6ca29 100644 --- a/ClassicalSharp/Rendering/MapRenderer.cs +++ b/ClassicalSharp/Rendering/MapRenderer.cs @@ -46,7 +46,7 @@ namespace ClassicalSharp.Renderers { Game game; IGraphicsApi gfx; - internal int _1DUsed = -1; + internal int _1DUsed = -1, chunksX, chunksY, chunksZ; internal int renderCount = 0; internal ChunkInfo[] chunks, renderChunks, unsortedChunks; internal bool[] usedTranslucent, usedNormal; @@ -67,12 +67,21 @@ namespace ClassicalSharp.Renderers { /// Discards any built meshes for all chunks in the map. public void Refresh() { updater.Refresh(); } - public void RedrawBlock(int x, int y, int z, BlockID block, int oldHeight, int newHeight) { - updater.RedrawBlock(x, y, z, block, oldHeight, newHeight); + /// Retrieves the information for the given chunk. + public ChunkInfo GetChunk(int cx, int cy, int cz) { + return unsortedChunks[cx + chunksX * (cy + cz * chunksY)]; } /// Marks the given chunk as needing to be deleted. - public void RefreshChunk(int cx, int cy, int cz) { updater.RefreshChunk(cx, cy, cz); } + public void RefreshChunk(int cx, int cy, int cz) { + if (cx < 0 || cy < 0 || cz < 0 || + cx >= chunksX || cy >= chunksY || cz >= chunksZ) return; + + ChunkInfo info = unsortedChunks[cx + chunksX * (cy + cz * chunksY)]; + if (info.AllAir) return; // do not recreate chunks completely air + info.Empty = false; + info.PendingDelete = true; + } /// Potentially generates meshes for several pending chunks. public void Update(double deltaTime) {