2016-10-17 16:42:38 +11:00

437 lines
14 KiB
C#

// ClassicalSharp copyright 2014-2016 UnknownShadow200 | Licensed under MIT
using System;
using ClassicalSharp.Entities;
using ClassicalSharp.Events;
using ClassicalSharp.GraphicsAPI;
using ClassicalSharp.Map;
using OpenTK;
namespace ClassicalSharp.Renderers {
/// <summary> Manages the process of building/deleting chunk meshes,
/// in addition to calculating the visibility of chunks. </summary>
public sealed class ChunkUpdater : IDisposable {
Game game;
ChunkMeshBuilder builder;
BlockInfo info;
int width, height, length;
internal int[] distances;
internal Vector3I chunkPos = new Vector3I( int.MaxValue );
int elementsPerBitmap = 0;
MapRenderer renderer;
public ChunkUpdater( Game game, MapRenderer renderer ) {
this.game = game;
this.renderer = renderer;
info = game.BlockInfo;
InitMeshBuilder();
game.Events.TerrainAtlasChanged += TerrainAtlasChanged;
game.WorldEvents.OnNewMap += OnNewMap;
game.WorldEvents.OnNewMapLoaded += OnNewMapLoaded;
game.WorldEvents.EnvVariableChanged += EnvVariableChanged;
game.Events.BlockDefinitionChanged += BlockDefinitionChanged;
game.Events.ViewDistanceChanged += ViewDistanceChanged;
game.Events.ProjectionChanged += ProjectionChanged;
game.Graphics.ContextLost += ContextLost;
game.Graphics.ContextRecreated += ContextRecreated;
}
public void InitMeshBuilder() {
if( builder != null ) builder.Dispose();
if( game.SmoothLighting ) {
builder = new AdvLightingMeshBuilder();
} else {
builder = new NormalMeshBuilder();
}
builder.Init( game );
builder.OnNewMapLoaded();
}
public void Dispose() {
ClearChunkCache();
renderer.chunks = null;
renderer.unsortedChunks = null;
game.Events.TerrainAtlasChanged -= TerrainAtlasChanged;
game.WorldEvents.OnNewMap -= OnNewMap;
game.WorldEvents.OnNewMapLoaded -= OnNewMapLoaded;
game.WorldEvents.EnvVariableChanged -= EnvVariableChanged;
game.Events.BlockDefinitionChanged -= BlockDefinitionChanged;
game.Events.ViewDistanceChanged -= ViewDistanceChanged;
game.Events.ProjectionChanged -= ProjectionChanged;
game.Graphics.ContextLost -= ContextLost;
game.Graphics.ContextRecreated -= ContextRecreated;
builder.Dispose();
}
public void Refresh() {
chunkPos = new Vector3I( int.MaxValue );
renderer.totalUsed = new int[game.TerrainAtlas1D.TexIds.Length];
if( renderer.chunks == null || game.World.IsNotLoaded ) return;
ClearChunkCache();
ResetChunkCache();
}
void RefreshBorders( int clipLevel ) {
chunkPos = new Vector3I( int.MaxValue );
if( renderer.chunks == null || game.World.IsNotLoaded ) return;
int index = 0;
for( int z = 0; z < chunksZ; z++ )
for( int y = 0; y < chunksY; y++ )
for( int x = 0; x < chunksX; x++ )
{
bool isBorder = x == 0 || z == 0 || x == (chunksX - 1) || z == (chunksZ - 1);
if( isBorder && (y * 16) < clipLevel )
DeleteChunk( renderer.unsortedChunks[index], true );
index++;
}
}
void EnvVariableChanged( object sender, EnvVarEventArgs e ) {
if( e.Var == EnvVar.SunlightColour || e.Var == EnvVar.ShadowlightColour ) {
Refresh();
} else if( e.Var == EnvVar.EdgeLevel ) {
int oldClip = builder.edgeLevel;
builder.sidesLevel = Math.Max( 0, game.World.Env.SidesHeight );
builder.edgeLevel = Math.Max( 0, game.World.Env.EdgeHeight );
RefreshBorders( Math.Max( oldClip, builder.edgeLevel ) );
}
}
void TerrainAtlasChanged( object sender, EventArgs e ) {
if( renderer._1DUsed == -1 ) {
renderer.totalUsed = new int[game.TerrainAtlas1D.TexIds.Length];
} else {
bool refreshRequired = elementsPerBitmap != game.TerrainAtlas1D.elementsPerBitmap;
if( refreshRequired ) Refresh();
}
renderer._1DUsed = game.TerrainAtlas1D.CalcMaxUsedRow( game.TerrainAtlas, info );
elementsPerBitmap = game.TerrainAtlas1D.elementsPerBitmap;
ResetUsedFlags();
}
void BlockDefinitionChanged( object sender, EventArgs e ) {
renderer._1DUsed = game.TerrainAtlas1D.CalcMaxUsedRow( game.TerrainAtlas, info );
ResetUsedFlags();
Refresh();
}
void ProjectionChanged( object sender, EventArgs e ) {
lastCamPos = Utils.MaxPos();
}
void OnNewMap( object sender, EventArgs e ) {
game.ChunkUpdates = 0;
ClearChunkCache();
for( int i = 0; i < renderer.totalUsed.Length; i++ )
renderer.totalUsed[i] = 0;
renderer.chunks = null;
renderer.unsortedChunks = null;
chunkPos = new Vector3I( int.MaxValue, int.MaxValue, int.MaxValue );
}
void ViewDistanceChanged( object sender, EventArgs e ) {
lastCamPos = Utils.MaxPos();
lastYaw = float.MaxValue;
lastPitch = float.MaxValue;
}
internal void ResetUsedFlags() {
int count = renderer._1DUsed;
bool[] used = renderer.usedTranslucent;
if( used == null || count > used.Length ) {
renderer.usedTranslucent = new bool[count];
renderer.usedNormal = new bool[count];
renderer.pendingTranslucent = new bool[count];
renderer.pendingNormal = new bool[count];
}
for( int i = 0; i < count; i++ ) {
renderer.pendingTranslucent[i] = true;
renderer.usedTranslucent[i] = false;
renderer.pendingNormal[i] = true;
renderer.usedNormal[i] = false;
}
}
int chunksX, chunksY, chunksZ;
void OnNewMapLoaded( object sender, EventArgs e ) {
width = NextMultipleOf16( game.World.Width );
height = NextMultipleOf16( game.World.Height );
length = NextMultipleOf16( game.World.Length );
chunksX = width >> 4;
chunksY = height >> 4;
chunksZ = length >> 4;
int count = chunksX * chunksY * chunksZ;
if( renderer.chunks == null || renderer.chunks.Length != count ) {
renderer.chunks = new ChunkInfo[count];
renderer.unsortedChunks = new ChunkInfo[count];
renderer.renderChunks = new ChunkInfo[count];
distances = new int[count];
}
CreateChunkCache();
builder.OnNewMapLoaded();
lastCamPos = Utils.MaxPos();
lastYaw = float.MaxValue;
lastPitch = float.MaxValue;
}
void CreateChunkCache() {
int index = 0;
for( int z = 0; z < length; z += 16 )
for( int y = 0; y < height; y += 16 )
for( int x = 0; x < width; x += 16 )
{
renderer.chunks[index] = new ChunkInfo( x, y, z );
renderer.unsortedChunks[index] = renderer.chunks[index];
renderer.renderChunks[index] = renderer.chunks[index];
distances[index] = 0;
index++;
}
}
void ResetChunkCache() {
int index = 0;
for( int z = 0; z < length; z += 16 )
for( int y = 0; y < height; y += 16 )
for( int x = 0; x < width; x += 16 )
{
renderer.unsortedChunks[index].Reset( x, y, z );
index++;
}
}
void ClearChunkCache() {
if( renderer.chunks == null ) return;
for( int i = 0; i < renderer.chunks.Length; i++ )
DeleteChunk( renderer.chunks[i], false );
renderer.totalUsed = new int[game.TerrainAtlas1D.TexIds.Length];
}
void DeleteChunk( ChunkInfo info, bool decUsed ) {
info.Empty = false;
#if OCCLUSION
info.OcclusionFlags = 0;
info.OccludedFlags = 0;
#endif
if( info.NormalParts != null )
DeleteData( ref info.NormalParts, decUsed );
if( info.TranslucentParts != null )
DeleteData( ref info.TranslucentParts, decUsed );
}
void DeleteData( ref ChunkPartInfo[] parts, bool decUsed ) {
if( decUsed ) DecrementUsed( parts );
for( int i = 0; i < parts.Length; i++ )
game.Graphics.DeleteVb( ref parts[i].VbId );
parts = null;
}
static int NextMultipleOf16( int value ) { return (value + 0x0F) & ~0x0F; }
void ContextLost() { ClearChunkCache(); }
void ContextRecreated() { Refresh(); }
public void RedrawBlock( int x, int y, int z, byte block, int oldHeight, int newHeight ) {
int cx = x >> 4, cy = y >> 4, cz = z >> 4;
// 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 ) ) )
ResetChunk( 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 ) ) )
ResetChunk( cx, cy + 1, cz );
if( bZ == 15 && cz < chunksZ - 1 )
ResetNeighbour( x, y, z + 1, block, cx, cy, cz + 1, minCy, maxCy );
}
bool Needs( byte block, byte other ) { return !info.IsOpaque[block] || !info.IsAir[other]; }
void ResetNeighbour( int x, int y, int z, byte 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, byte 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--) {
byte other = world.blocks[index];
bool affected = y == nY ? Needs( block, other ) : !info.IsAir[other];
if( affected ) { ResetChunk( cx, cy, cz ); return; }
index -= world.Width * world.Length;
}
}
void ResetColumn( int cx, int cy, int cz, int minCy, int maxCy ) {
if( minCy == maxCy ) {
ResetChunk( cx, cy, cz );
} else {
for( cy = maxCy; cy >= minCy; cy-- )
ResetChunk( cx, cy, cz );
}
}
void ResetChunk( int cx, int cy, int cz ) {
if( cx < 0 || cy < 0 || cz < 0 ||
cx >= chunksX || cy >= chunksY || cz >= chunksZ ) return;
DeleteChunk( renderer.unsortedChunks[cx + chunksX * (cy + cz * chunksY)], true );
}
int chunksTarget = 4;
const double targetTime = (1.0 / 30) + 0.01;
public void UpdateChunks( double delta ) {
int chunkUpdates = 0;
chunksTarget += delta < targetTime ? 1 : -1; // build more chunks if 30 FPS or over, otherwise slowdown.
Utils.Clamp( ref chunksTarget, 4, 16 );
LocalPlayer p = game.LocalPlayer;
Vector3 cameraPos = game.CurrentCameraPos;
bool samePos = cameraPos == lastCamPos && p.HeadYawDegrees == lastYaw
&& p.PitchDegrees == lastPitch;
renderer.renderCount = samePos ? UpdateChunksStill( ref chunkUpdates ) :
UpdateChunksAndVisibility( ref chunkUpdates );
lastCamPos = cameraPos;
lastYaw = p.HeadYawDegrees; lastPitch = p.PitchDegrees;
if( !samePos || chunkUpdates != 0 )
ResetUsedFlags();
}
Vector3 lastCamPos;
float lastYaw, lastPitch;
int UpdateChunksAndVisibility( ref int chunkUpdates ) {
ChunkInfo[] chunks = renderer.chunks, render = renderer.renderChunks;
int j = 0;
int viewDistSqr = AdjustViewDist( game.ViewDistance );
int userDistSqr = AdjustViewDist( game.UserViewDistance );
for( int i = 0; i < chunks.Length; i++ ) {
ChunkInfo info = chunks[i];
if( info.Empty ) continue;
int distSqr = distances[i];
bool noData = info.NormalParts == null && info.TranslucentParts == null;
if( !noData && distSqr >= userDistSqr + 32 * 16 ) {
DeleteChunk( info, true ); continue;
}
if( noData && distSqr <= viewDistSqr && chunkUpdates < chunksTarget ) {
BuildChunk( info, ref chunkUpdates );
}
info.Visible = distSqr <= viewDistSqr &&
game.Culling.SphereInFrustum( info.CentreX, info.CentreY, info.CentreZ, 14 ); // 14 ~ sqrt(3 * 8^2)
if( info.Visible && !info.Empty ) { render[j] = info; j++; }
}
return j;
}
int UpdateChunksStill( ref int chunkUpdates ) {
ChunkInfo[] chunks = renderer.chunks, render = renderer.renderChunks;
int j = 0;
int viewDistSqr = AdjustViewDist( game.ViewDistance );
int userDistSqr = AdjustViewDist( game.UserViewDistance );
for( int i = 0; i < chunks.Length; i++ ) {
ChunkInfo info = chunks[i];
if( info.Empty ) continue;
int distSqr = distances[i];
bool noData = info.NormalParts == null && info.TranslucentParts == null;
if( !noData && distSqr >= userDistSqr + 32 * 16 ) {
DeleteChunk( info, true ); continue;
}
if( noData ) {
if( distSqr <= userDistSqr && chunkUpdates < chunksTarget ) {
BuildChunk( info, ref chunkUpdates );
// only need to update the visibility of chunks in range.
info.Visible = distSqr <= viewDistSqr &&
game.Culling.SphereInFrustum( info.CentreX, info.CentreY, info.CentreZ, 14 ); // 14 ~ sqrt(3 * 8^2)
if( info.Visible && !info.Empty ) { render[j] = info; j++; }
}
} else if ( info.Visible ) {
render[j] = info; j++;
}
}
return j;
}
static int AdjustViewDist( float dist ) {
int viewDist = Utils.AdjViewDist( Math.Max( 16, dist ) );
return (viewDist + 24) * (viewDist + 24);
}
void BuildChunk( ChunkInfo info, ref int chunkUpdates ) {
game.ChunkUpdates++;
builder.GetDrawInfo( info.CentreX - 8, info.CentreY - 8, info.CentreZ - 8,
ref info.NormalParts, ref info.TranslucentParts );
if( info.NormalParts == null && info.TranslucentParts == null ) {
info.Empty = true;
} else {
if( info.NormalParts != null )
IncrementUsed( info.NormalParts );
if ( info.TranslucentParts != null )
IncrementUsed( info.TranslucentParts );
}
chunkUpdates++;
}
void IncrementUsed( ChunkPartInfo[] parts ) {
for( int i = 0; i < parts.Length; i++ ) {
if( parts[i].IndicesCount == 0 ) continue;
renderer.totalUsed[i]++;
}
}
void DecrementUsed( ChunkPartInfo[] parts ) {
for( int i = 0; i < parts.Length; i++ ) {
if( parts[i].IndicesCount == 0 ) continue;
renderer.totalUsed[i]--;
}
}
}
}