using System; using ClassicalSharp.GraphicsAPI; using OpenTK; namespace ClassicalSharp { // TODO: optimise chunk rendering // --> reduce iterations: liquid and sprite pass only need 1 row public partial class MapRenderer : IDisposable { class ChunkInfo { public ushort CentreX, CentreY, CentreZ; public bool Visible = true; public bool Empty = false; public bool DrawLeft, DrawRight, DrawFront, DrawBack, DrawBottom, DrawTop; public byte OcclusionFlags, VisibilityFlags; public ChunkPartInfo[] NormalParts; public ChunkPartInfo[] TranslucentParts; public ChunkInfo( int x, int y, int z ) { CentreX = (ushort)( x + 8 ); CentreY = (ushort)( y + 8 ); CentreZ = (ushort)( z + 8 ); } } Game game; IGraphicsApi api; int _1Dcount = 1, _1DUsed = 1; ChunkMeshBuilder builder; BlockInfo info; int width, height, length; ChunkInfo[] chunks, unsortedChunks; Vector3I chunkPos = new Vector3I( int.MaxValue, int.MaxValue, int.MaxValue ); int elementsPerBitmap = 0; public MapRenderer( Game game ) { this.game = game; _1Dcount = game.TerrainAtlas1D.TexIds.Length; _1DUsed = game.TerrainAtlas1D.CalcMaxUsedRow( game.TerrainAtlas, game.BlockInfo ); builder = new ChunkMeshBuilder( game ); api = game.Graphics; elementsPerBitmap = game.TerrainAtlas1D.elementsPerBitmap; info = game.BlockInfo; game.Events.TerrainAtlasChanged += TerrainAtlasChanged; game.Events.OnNewMap += OnNewMap; game.Events.OnNewMapLoaded += OnNewMapLoaded; game.Events.EnvVariableChanged += EnvVariableChanged; game.Events.BlockDefinitionChanged += BlockDefinitionChanged; } public void Dispose() { ClearChunkCache(); chunks = null; unsortedChunks = null; game.Events.TerrainAtlasChanged -= TerrainAtlasChanged; game.Events.OnNewMap -= OnNewMap; game.Events.OnNewMapLoaded -= OnNewMapLoaded; game.Events.EnvVariableChanged -= EnvVariableChanged; game.Events.BlockDefinitionChanged -= BlockDefinitionChanged; builder.Dispose(); } public void Refresh() { if( chunks != null && !game.Map.IsNotLoaded ) { ClearChunkCache(); CreateChunkCache(); } chunkPos = new Vector3I( int.MaxValue, int.MaxValue, int.MaxValue ); } void EnvVariableChanged( object sender, EnvVarEventArgs e ) { if( e.Var == EnvVar.SunlightColour || e.Var == EnvVar.ShadowlightColour ) { Refresh(); } else if( e.Var == EnvVar.WaterLevel ) { builder.clipLevel = Math.Max( 0, game.Map.GroundHeight ); Refresh(); } } void TerrainAtlasChanged( object sender, EventArgs e ) { _1Dcount = game.TerrainAtlas1D.TexIds.Length; bool fullResetRequired = elementsPerBitmap != game.TerrainAtlas1D.elementsPerBitmap; if( fullResetRequired ) { Refresh(); } elementsPerBitmap = game.TerrainAtlas1D.elementsPerBitmap; _1DUsed = game.TerrainAtlas1D.CalcMaxUsedRow( game.TerrainAtlas, game.BlockInfo ); } void BlockDefinitionChanged( object sender, EventArgs e ) { _1DUsed = game.TerrainAtlas1D.CalcMaxUsedRow( game.TerrainAtlas, game.BlockInfo ); } void OnNewMap( object sender, EventArgs e ) { game.ChunkUpdates = 0; ClearChunkCache(); chunks = null; unsortedChunks = null; chunkPos = new Vector3I( int.MaxValue, int.MaxValue, int.MaxValue ); builder.OnNewMap(); } int chunksX, chunksY, chunksZ; void OnNewMapLoaded( object sender, EventArgs e ) { width = NextMultipleOf16( game.Map.Width ); height = NextMultipleOf16( game.Map.Height ); length = NextMultipleOf16( game.Map.Length ); chunksX = width >> 4; chunksY = height >> 4; chunksZ = length >> 4; chunks = new ChunkInfo[chunksX * chunksY * chunksZ]; unsortedChunks = new ChunkInfo[chunksX * chunksY * chunksZ]; distances = new int[chunks.Length]; CreateChunkCache(); builder.OnNewMapLoaded(); } void ClearChunkCache() { if( chunks == null ) return; for( int i = 0; i < chunks.Length; i++ ) { DeleteChunk( chunks[i] ); } } void DeleteChunk( ChunkInfo info ) { info.Empty = false; info.OcclusionFlags = 0; info.VisibilityFlags = 0; DeleteData( ref info.NormalParts ); DeleteData( ref info.TranslucentParts ); } void DeleteData( ref ChunkPartInfo[] parts ) { if( parts == null ) return; for( int i = 0; i < parts.Length; i++ ) { api.DeleteVb( parts[i].VbId ); } parts = null; } 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 ) { chunks[index] = new ChunkInfo( x, y, z ); unsortedChunks[index] = chunks[index]; index++; } } } } static int NextMultipleOf16( int value ) { return (value + 0x0F) & ~0x0F; } public void RedrawBlock( int x, int y, int z, byte block, int oldHeight, int newHeight ) { int cx = x >> 4, bX = x & 0x0F; int cy = y >> 4, bY = y & 0x0F; int cz = z >> 4, bZ = z & 0x0F; // 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 newLightcy = newHeight < 0 ? 0 : newHeight >> 4; int oldLightcy = oldHeight < 0 ? 0 : oldHeight >> 4; ResetChunkAndBelow( cx, cy, cz, newLightcy, oldLightcy ); if( bX == 0 && cx > 0 && NeedsUpdate( x, y, z, x - 1, y, z ) ) ResetChunkAndBelow( cx - 1, cy, cz, newLightcy, oldLightcy ); if( bY == 0 && cy > 0 && NeedsUpdate( x, y, z, x, y - 1, z ) ) ResetChunkAndBelow( cx, cy - 1, cz, newLightcy, oldLightcy ); if( bZ == 0 && cz > 0 && NeedsUpdate( x, y, z, x, y, z - 1 ) ) ResetChunkAndBelow( cx, cy, cz - 1, newLightcy, oldLightcy ); if( bX == 15 && cx < chunksX - 1 && NeedsUpdate( x, y, z, x + 1, y, z ) ) ResetChunkAndBelow( cx + 1, cy, cz, newLightcy, oldLightcy ); if( bY == 15 && cy < chunksY - 1 && NeedsUpdate( x, y, z, x, y + 1, z ) ) ResetChunkAndBelow( cx, cy + 1, cz, newLightcy, oldLightcy ); if( bZ == 15 && cz < chunksZ - 1 && NeedsUpdate( x, y, z, x, y, z + 1 ) ) ResetChunkAndBelow( cx, cy, cz + 1, newLightcy, oldLightcy ); } bool NeedsUpdate( int x1, int y1, int z1, int x2, int y2, int z2 ) { byte b1 = game.Map.SafeGetBlock( x1, y1, z1 ); byte b2 = game.Map.SafeGetBlock( x2, y2, z2 ); return (!info.IsOpaque[b1] && info.IsOpaque[b2]) || !(info.IsOpaque[b1] && b2 == 0); } void ResetChunkAndBelow( int cx, int cy, int cz, int newLightCy, int oldLightCy ) { if( newLightCy == oldLightCy ) { ResetChunk( cx, cy, cz ); } else { int cyMax = Math.Max( newLightCy, oldLightCy ); int cyMin = Math.Min( oldLightCy, newLightCy ); for( cy = cyMax; cy >= cyMin; 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( unsortedChunks[cx + chunksX * ( cy + cz * chunksY )] ); } public void Render( double deltaTime ) { if( chunks == null ) return; UpdateSortOrder(); UpdateChunks( deltaTime ); //SimpleOcclusionCulling(); RenderNormal(); game.MapEnvRenderer.Render( deltaTime ); RenderTranslucent(); } int[] distances; void UpdateSortOrder() { Player p = game.LocalPlayer; Vector3I newChunkPos = Vector3I.Floor( p.EyePosition ); newChunkPos.X = ( newChunkPos.X & ~0x0F ) + 8; newChunkPos.Y = ( newChunkPos.Y & ~0x0F ) + 8; newChunkPos.Z = ( newChunkPos.Z & ~0x0F ) + 8; if( newChunkPos == chunkPos ) return; chunkPos = newChunkPos; for( int i = 0; i < distances.Length; i++ ) { ChunkInfo info = chunks[i]; distances[i] = Utils.DistanceSquared( info.CentreX, info.CentreY, info.CentreZ, chunkPos.X, chunkPos.Y, chunkPos.Z ); } // NOTE: Over 5x faster compared to normal comparison of IComparer.Compare Array.Sort( distances, chunks ); Vector3I pPos = newChunkPos; for( int i = 0; i < chunks.Length; i++ ) { ChunkInfo info = chunks[i]; int dX1 = (info.CentreX - 8) - pPos.X, dX2 = (info.CentreX + 8) - pPos.X; int dY1 = (info.CentreY - 8) - pPos.Y, dY2 = (info.CentreY + 8) - pPos.Y; int dZ1 = (info.CentreZ - 8) - pPos.Z, dZ2 = (info.CentreZ + 8) - pPos.Z; // Back face culling: make sure that the chunk is definitely entirely back facing. info.DrawLeft = !(dX1 <= 0 && dX2 <= 0); info.DrawRight = !(dX1 >= 0 && dX2 >= 0); info.DrawFront = !(dZ1 <= 0 && dZ2 <= 0); info.DrawBack = !(dZ1 >= 0 && dZ2 >= 0); info.DrawBottom = !(dY1 <= 0 && dY2 <= 0); info.DrawTop = !(dY1 >= 0 && dY2 >= 0); } } int chunksTarget = 4; const double targetTime = (1.0 / 30) + 0.01; void UpdateChunks( double deltaTime ) { int chunksUpdatedThisFrame = 0; int adjViewDistSqr = ( game.ViewDistance + 14 ) * ( game.ViewDistance + 14 ); chunksTarget += deltaTime < targetTime ? 1 : -1; // build more chunks if 30 FPS or over, otherwise slowdown. Utils.Clamp( ref chunksTarget, 4, 12 ); for( int i = 0; i < chunks.Length; i++ ) { ChunkInfo info = chunks[i]; if( info.Empty ) continue; int distSqr = distances[i]; bool inRange = distSqr <= adjViewDistSqr; if( info.NormalParts == null && info.TranslucentParts == null ) { if( inRange && chunksUpdatedThisFrame < chunksTarget ) { game.ChunkUpdates++; builder.GetDrawInfo( info.CentreX - 8, info.CentreY - 8, info.CentreZ - 8, ref info.NormalParts, ref info.TranslucentParts, ref info.OcclusionFlags ); if( info.NormalParts == null && info.TranslucentParts == null ) info.Empty = true; chunksUpdatedThisFrame++; } } info.Visible = inRange && game.Culling.SphereInFrustum( info.CentreX, info.CentreY, info.CentreZ, 14 ); // 14 ~ sqrt(3 * 8^2) } } // Render solid and fully transparent to fill depth buffer. // These blocks are treated as having an alpha value of either none or full. void RenderNormal() { int[] texIds = game.TerrainAtlas1D.TexIds; api.BeginVbBatch( VertexFormat.Pos3fTex2fCol4b ); api.Texturing = true; api.AlphaTest = true; for( int batch = 0; batch < _1DUsed; batch++ ) { api.BindTexture( texIds[batch] ); RenderNormalBatch( batch ); } api.AlphaTest = false; api.Texturing = false; } // Render translucent(liquid) blocks. These 'blend' into other blocks. void RenderTranslucent() { Block block = game.LocalPlayer.BlockAtHead; drawAllFaces = block == Block.Water || block == Block.StillWater; // First fill depth buffer int[] texIds = game.TerrainAtlas1D.TexIds; api.BeginVbBatch( VertexFormat.Pos3fTex2fCol4b ); api.Texturing = false; api.AlphaBlending = false; api.ColourWrite = false; for( int batch = 0; batch < _1DUsed; batch++ ) { RenderTranslucentBatchDepthPass( batch ); } // Then actually draw the transluscent blocks api.AlphaBlending = true; api.Texturing = true; api.ColourWrite = true; api.DepthWrite = false; // we already calculated depth values in depth pass for( int batch = 0; batch < _1DUsed; batch++ ) { api.BindTexture( texIds[batch] ); RenderTranslucentBatch( batch ); } api.DepthWrite = true; api.AlphaTest = false; api.AlphaBlending = false; api.Texturing = false; } void SimpleOcclusionCulling() { // TODO: broken Vector3 p = game.LocalPlayer.EyePosition; Vector3I chunkLoc = Vector3I.Floor( p ); ChunkInfo chunkIn = null; byte chunkInFlags = 0; // We have to pretend that the chunk the player is in does no occlusion // (because for example, only X15 may be filled while X0 is air) if( game.Map.IsValidPos( chunkLoc ) ) { int cx = chunkLoc.X >> 4; int cy = chunkLoc.Y >> 4; int cz = chunkLoc.Z >> 4; chunkIn = unsortedChunks[cx + chunksX * (cy + cz * chunksY)]; chunkInFlags = chunkIn.OcclusionFlags; chunkIn.OcclusionFlags = 0; } for( int i = 0; i < chunks.Length; i++ ) { ChunkInfo chunk = chunks[i]; chunk.VisibilityFlags = chunk.OcclusionFlags; int x1 = chunk.CentreX - 8, x2 = chunk.CentreX + 8; int y1 = chunk.CentreY - 8, y2 = chunk.CentreY + 8; int z1 = chunk.CentreZ - 8, z2 = chunk.CentreZ + 8; int xOffset = 0, yOffset = 0, zOffset = 0; float dx = 0, dy = 0, dz = 0; float distX, distY, distZ; // TODO: two axes with same distance // X axis collisions dy = Math.Max( y1 - p.Y, Math.Max( 0, p.Y - y2 ) ); dz = Math.Max( z1 - p.Z, Math.Max( 0, p.Z - z2 ) ); float dxLeft = Math.Max( x1 - p.X, Math.Max( 0, p.X - x1 ) ); float dxRight = Math.Max( x2 - p.X, Math.Max( 0, p.X - x2 ) ); if( dxLeft < dxRight ) { xOffset = -1; distX = dxLeft * dxLeft + dy * dy + dz * dz; } else { xOffset = 1; distX = dxRight * dxRight + dy * dy + dz * dz; } // Z axis collisions dx = Math.Max( x1 - p.X, Math.Max( 0, p.X - x2 ) ); dy = Math.Max( y1 - p.Y, Math.Max( 0, p.Y - y2 ) ); float dxFront = Math.Max( z1 - p.Z, Math.Max( 0, p.Z - z1 ) ); float dxBack = Math.Max( z2 - p.Z, Math.Max( 0, p.Z - z2 ) ); if( dxFront < dxBack ) { zOffset = -1; distZ = dx * dx + dy * dy + dxFront * dxFront; } else { zOffset = 1; distZ = dx * dx + dy * dy + dxBack * dxBack; } // Y axis collisions dx = Math.Max( x1 - p.X, Math.Max( 0, p.X - x2 ) ); dz = Math.Max( z1 - p.Z, Math.Max( 0, p.Z - z2 ) ); float dxBottom = Math.Max( y1 - p.Y, Math.Max( 0, p.Y - y1 ) ); float dxTop = Math.Max( y2 - p.Y, Math.Max( 0, p.Y - y2 ) ); if( dxBottom < dxTop ) { yOffset = -1; distY = dx * dx + dxBottom * dxBottom + dz * dz; } else { yOffset = 1; distY = dx * dx + dxTop * dxTop + dz * dz; } int cx = chunk.CentreX >> 4; int cy = chunk.CentreY >> 4; int cz = chunk.CentreZ >> 4; float distMin = Math.Min( distX, Math.Min( distY, distZ ) ); bool occlude = true; byte flags = 0; if( Math.Abs( distMin - distX ) < 0.00001f ) OccludeX( cx, cy, cz, xOffset, ref occlude, ref flags ); if( Math.Abs( distMin - distZ ) < 0.00001f ) OccludeZ( cx, cy, cz, zOffset, ref occlude, ref flags ); if( Math.Abs( distMin - distY ) < 0.00001f ) OccludeY( cx, cy, cz, yOffset, ref occlude, ref flags ); if( occlude ) { chunk.Visible = false; chunk.VisibilityFlags = flags; } } if( chunkIn != null ) { chunkIn.Visible = true; chunkIn.OcclusionFlags = chunkInFlags; } } void OccludeX( int cx, int cy, int cz, int xOffset, ref bool occlude, ref byte flags ) { cx += xOffset; if( cx >= 0 && cx < chunksX ) { ChunkInfo neighbour = unsortedChunks[cx + chunksX * (cy + cz * chunksY)]; if( (neighbour.VisibilityFlags & 0x1) == 0 ) occlude = false; else flags |= 0x1; } } void OccludeZ( int cx, int cy, int cz, int zOffset, ref bool occlude, ref byte flags ) { cz += zOffset; if( cz >= 0 && cz < chunksZ ) { ChunkInfo neighbour = unsortedChunks[cx + chunksX * (cy + cz * chunksY)]; if( (neighbour.VisibilityFlags & 0x2) == 0 ) occlude = false; else flags |= 0x2; } } void OccludeY( int cx, int cy, int cz, int yOffset, ref bool occlude, ref byte flags ) { cy += yOffset; if( cy >= 0 && cy< chunksY ) { ChunkInfo neighbour = unsortedChunks[cx + chunksX * (cy + cz * chunksY)]; if( (neighbour.VisibilityFlags & 0x4) == 0 ) occlude = false; else flags |= 0x4; } } static float DistToRecSquared( Vector3 p, int x1, int y1, int z1, int x2, int y2, int z2 ) { float dx = Math.Max( x1 - p.X, Math.Max( 0, p.X - x2 ) ); float dy = Math.Max( y1 - p.Y, Math.Max( 0, p.Y - y2 ) ); float dz = Math.Max( z1 - p.Z, Math.Max( 0, p.Z - z2 ) ); return dx * dx + dy * dy + dz * dz; } } }