Modularise lighting more

This commit is contained in:
UnknownShadow200 2017-04-22 15:03:06 +10:00
parent 08d38fd348
commit 747e3183d6
10 changed files with 165 additions and 150 deletions

View File

@ -187,6 +187,7 @@
<Compile Include="Map\Formats\MapSchematic.Exporter.cs" /> <Compile Include="Map\Formats\MapSchematic.Exporter.cs" />
<Compile Include="Map\Formats\MapLvl.Importer.cs" /> <Compile Include="Map\Formats\MapLvl.Importer.cs" />
<Compile Include="Map\Formats\NbtFile.cs" /> <Compile Include="Map\Formats\NbtFile.cs" />
<Compile Include="Map\Lighting\BasicLighting.Updater.cs" />
<Compile Include="Map\Lighting\IWorldLighting.cs" /> <Compile Include="Map\Lighting\IWorldLighting.cs" />
<Compile Include="Map\Lighting\BasicLighting.cs" /> <Compile Include="Map\Lighting\BasicLighting.cs" />
<Compile Include="Map\Lighting\BasicLighting.Heightmap.cs" /> <Compile Include="Map\Lighting\BasicLighting.Heightmap.cs" />

View File

@ -283,17 +283,18 @@ namespace ClassicalSharp {
} }
public void UpdateBlock(int x, int y, int z, BlockID block) { 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); BlockID oldBlock = World.GetBlock(x, y, z);
World.SetBlock(x, y, z, block); World.SetBlock(x, y, z, block);
Lighting.UpdateLight(x, y, z, oldBlock, block);
WeatherRenderer weather = WeatherRenderer; WeatherRenderer weather = WeatherRenderer;
if (weather.heightmap != null && !World.IsNotLoaded) if (weather.heightmap != null && !World.IsNotLoaded)
weather.OnBlockChanged(x, y, z, oldBlock, block); weather.OnBlockChanged(x, y, z, oldBlock, block);
Lighting.OnBlockChanged(x, y, z, oldBlock, block);
int newHeight = Lighting.GetLightHeight(x, z) + 1; // Refresh the chunk the block was located in.
MapRenderer.RedrawBlock(x, y, z, block, oldHeight, newHeight); 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; float limitMilliseconds;

View File

@ -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 {
/// <summary> Manages lighting through a simple heightmap, where each block is either in sun or shadow. </summary>
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);
}
}
}
}

View File

@ -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 index = (z * width) + x;
int lightH = heightmap[index]; int lightH = heightmap[index];
return lightH == short.MaxValue ? CalcHeightAt(x, height - 1, z, index) : lightH; 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++) for (int i = 0; i < heightmap.Length; i++)
heightmap[i] = short.MaxValue; 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);
}
}
}
} }
} }

View File

@ -24,13 +24,10 @@ namespace ClassicalSharp.Map {
// Except this function is a lot more optimised and minimises cache misses. // Except this function is a lot more optimised and minimises cache misses.
public unsafe abstract void LightHint(int startX, int startZ, BlockID* mapPtr); public unsafe abstract void LightHint(int startX, int startZ, BlockID* mapPtr);
/// <summary> Returns the y coordinate of the highest block that is fully not in sunlight. </summary> /// <summary> Called when a block is changed, to update the lighting information. </summary>
/// <remarks> *** Does NOT check that the coordinates are inside the map. *** <br/> /// <remarks> Derived classes ***MUST*** mark all chunks affected by this lighting change
/// e.g. if cobblestone was at y = 5, this method would return 4. </remarks> /// as needing to be refreshed. </remarks>
public abstract int GetLightHeight(int x, int z); public abstract void OnBlockChanged(int x, int y, int z, BlockID oldBlock, BlockID newBlock);
/// <summary> Updates the lighting for the block at that position, which may in turn affect other blocks. </summary>
public abstract void UpdateLight(int x, int y, int z, BlockID oldBlock, BlockID newBlock);
/// <summary> Discards all cached lighting information. </summary> /// <summary> Discards all cached lighting information. </summary>
public virtual void Refresh() { } public virtual void Refresh() { }

View File

@ -71,7 +71,7 @@ namespace ClassicalSharp.Map {
/// <summary> Sets the block at the given world coordinates without bounds checking, /// <summary> Sets the block at the given world coordinates without bounds checking,
/// and also recalculates the heightmap for the given (x,z) column. </summary> /// and also recalculates the heightmap for the given (x,z) column. </summary>
public void SetBlock(Vector3I p, BlockID blockId) { 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;
} }
/// <summary> Returns the block at the given world coordinates without bounds checking. </summary> /// <summary> Returns the block at the given world coordinates without bounds checking. </summary>

View File

@ -68,6 +68,7 @@ namespace ClassicalSharp.Network.Protocols {
byte HandleDefineBlockCommonStart(NetReader reader, bool uniqueSideTexs) { byte HandleDefineBlockCommonStart(NetReader reader, bool uniqueSideTexs) {
byte id = reader.ReadUInt8(); byte id = reader.ReadUInt8();
BlockInfo info = game.BlockInfo; BlockInfo info = game.BlockInfo;
bool didBlockLight = info.BlocksLight[id];
info.ResetBlockProps(id); info.ResetBlockProps(id);
info.Name[id] = reader.ReadString(); info.Name[id] = reader.ReadString();
@ -85,8 +86,7 @@ namespace ClassicalSharp.Network.Protocols {
} }
info.SetTex(reader.ReadUInt8(), Side.Bottom, id); info.SetTex(reader.ReadUInt8(), Side.Bottom, id);
// Need to refresh lighting when a block's light blocking state changes // Need to refresh lighting when a block's light blocking state changes
bool didBlockLight = info.BlocksLight[id];
info.BlocksLight[id] = reader.ReadUInt8() == 0; info.BlocksLight[id] = reader.ReadUInt8() == 0;
if (!game.World.IsNotLoaded && (didBlockLight != info.BlocksLight[id])) { if (!game.World.IsNotLoaded && (didBlockLight != info.BlocksLight[id])) {
game.Lighting.Refresh(); game.Lighting.Refresh();

View File

@ -160,9 +160,10 @@ namespace ClassicalSharp.Renderers {
width = NextMultipleOf16(game.World.Width); width = NextMultipleOf16(game.World.Width);
height = NextMultipleOf16(game.World.Height); height = NextMultipleOf16(game.World.Height);
length = NextMultipleOf16(game.World.Length); length = NextMultipleOf16(game.World.Length);
chunksX = width >> 4;
chunksY = height >> 4; chunksX = width >> 4; renderer.chunksX = chunksX;
chunksZ = length >> 4; chunksY = height >> 4; renderer.chunksY = chunksY;
chunksZ = length >> 4; renderer.chunksZ = chunksZ;
int count = chunksX * chunksY * chunksZ; int count = chunksX * chunksY * chunksZ;
if (renderer.chunks == null || renderer.chunks.Length != count) { if (renderer.chunks == null || renderer.chunks.Length != count) {
@ -236,90 +237,6 @@ namespace ClassicalSharp.Renderers {
void ContextLost() { ClearChunkCache(); } void ContextLost() { ClearChunkCache(); }
void ContextRecreated() { Refresh(); } 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; int chunksTarget = 12;
const double targetTime = (1.0 / 30) + 0.01; const double targetTime = (1.0 / 30) + 0.01;

View File

@ -191,10 +191,10 @@ namespace ClassicalSharp.Renderers {
int index = (x * length) + z; int index = (x * length) + z;
int height = heightmap[index]; int height = heightmap[index];
// Two cases can be skipped: // Two cases can be skipped here:
// a) rain height hadn't been calculated to begin with // a) rain height was not calculated to begin with (height is short.MaxValue)
// b) modified is below current rain height // b) changed y is below current calculated rain height
if (height == short.MaxValue || y < height) return; if (y < height) return;
if (nowBlock) { if (nowBlock) {
// Simple case: Rest of column below is now not visible to rain. // Simple case: Rest of column below is now not visible to rain.

View File

@ -46,7 +46,7 @@ namespace ClassicalSharp.Renderers {
Game game; Game game;
IGraphicsApi gfx; IGraphicsApi gfx;
internal int _1DUsed = -1; internal int _1DUsed = -1, chunksX, chunksY, chunksZ;
internal int renderCount = 0; internal int renderCount = 0;
internal ChunkInfo[] chunks, renderChunks, unsortedChunks; internal ChunkInfo[] chunks, renderChunks, unsortedChunks;
internal bool[] usedTranslucent, usedNormal; internal bool[] usedTranslucent, usedNormal;
@ -67,12 +67,21 @@ namespace ClassicalSharp.Renderers {
/// <summary> Discards any built meshes for all chunks in the map.</summary> /// <summary> Discards any built meshes for all chunks in the map.</summary>
public void Refresh() { updater.Refresh(); } public void Refresh() { updater.Refresh(); }
public void RedrawBlock(int x, int y, int z, BlockID block, int oldHeight, int newHeight) { /// <summary> Retrieves the information for the given chunk. </summary>
updater.RedrawBlock(x, y, z, block, oldHeight, newHeight); public ChunkInfo GetChunk(int cx, int cy, int cz) {
return unsortedChunks[cx + chunksX * (cy + cz * chunksY)];
} }
/// <summary> Marks the given chunk as needing to be deleted. </summary> /// <summary> Marks the given chunk as needing to be deleted. </summary>
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;
}
/// <summary> Potentially generates meshes for several pending chunks. </summary> /// <summary> Potentially generates meshes for several pending chunks. </summary>
public void Update(double deltaTime) { public void Update(double deltaTime) {