ambient occlusion: on demand

This is way faster now and almost disappeared in the cpu profiling.
It also renders ao only on touching faces
This commit is contained in:
Moritz Zwerger 2025-02-08 20:40:38 +01:00
parent b174638aaf
commit 1b869e4761
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
5 changed files with 116 additions and 50 deletions

View File

@ -34,11 +34,12 @@ import de.bixilon.minosoft.data.world.chunk.chunk.Chunk
import de.bixilon.minosoft.data.world.chunk.light.SectionLight import de.bixilon.minosoft.data.world.chunk.light.SectionLight
import de.bixilon.minosoft.data.world.chunk.neighbours.ChunkNeighbours import de.bixilon.minosoft.data.world.chunk.neighbours.ChunkNeighbours
import de.bixilon.minosoft.data.world.positions.BlockPosition import de.bixilon.minosoft.data.world.positions.BlockPosition
import de.bixilon.minosoft.data.world.positions.InChunkSectionPosition
import de.bixilon.minosoft.gui.rendering.RenderContext import de.bixilon.minosoft.gui.rendering.RenderContext
import de.bixilon.minosoft.gui.rendering.chunk.entities.BlockEntityRenderer import de.bixilon.minosoft.gui.rendering.chunk.entities.BlockEntityRenderer
import de.bixilon.minosoft.gui.rendering.chunk.entities.renderer.RenderedBlockEntity import de.bixilon.minosoft.gui.rendering.chunk.entities.renderer.RenderedBlockEntity
import de.bixilon.minosoft.gui.rendering.chunk.mesh.ChunkMeshes import de.bixilon.minosoft.gui.rendering.chunk.mesh.ChunkMeshes
import de.bixilon.minosoft.gui.rendering.light.AmbientOcclusionUtil import de.bixilon.minosoft.gui.rendering.light.ao.AmbientOcclusion
import de.bixilon.minosoft.gui.rendering.models.block.state.render.WorldRenderProps import de.bixilon.minosoft.gui.rendering.models.block.state.render.WorldRenderProps
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import java.util.* import java.util.*
@ -68,6 +69,7 @@ class SolidSectionMesher(
val entities: ArrayList<BlockEntityRenderer<*>> = ArrayList(section.blockEntities.count) val entities: ArrayList<BlockEntityRenderer<*>> = ArrayList(section.blockEntities.count)
val position = BlockPosition() val position = BlockPosition()
val inSectionPosition = InChunkSectionPosition()
val neighbourBlocks: Array<BlockState?> = arrayOfNulls(Directions.SIZE) val neighbourBlocks: Array<BlockState?> = arrayOfNulls(Directions.SIZE)
val light = ByteArray(Directions.SIZE + 1) // last index (6) for the current block val light = ByteArray(Directions.SIZE + 1) // last index (6) for the current block
@ -79,15 +81,17 @@ class SolidSectionMesher(
val floatOffset = FloatArray(3) val floatOffset = FloatArray(3)
val ao = Array(Directions.SIZE) { IntArray(4) } val ao = if (ambientOcclusion) AmbientOcclusion(section) else null
val props = WorldRenderProps(position, floatOffset, mesh, random, neighbourBlocks, light, ao) val props = WorldRenderProps(position, inSectionPosition, floatOffset, mesh, random, neighbourBlocks, light, ao)
for (y in blocks.minPosition.y..blocks.maxPosition.y) { for (y in blocks.minPosition.y..blocks.maxPosition.y) {
inSectionPosition.y = y
position.y = offsetY + y position.y = offsetY + y
floatOffset[1] = (position.y - cameraOffset.y).toFloat() floatOffset[1] = (position.y - cameraOffset.y).toFloat()
val fastBedrock = y == 0 && isLowestSection && fastBedrock val fastBedrock = y == 0 && isLowestSection && fastBedrock
for (x in blocks.minPosition.x..blocks.maxPosition.x) { for (x in blocks.minPosition.x..blocks.maxPosition.x) {
inSectionPosition.x = x
position.x = offsetX + x position.x = offsetX + x
floatOffset[0] = (position.x - cameraOffset.x).toFloat() floatOffset[0] = (position.x - cameraOffset.x).toFloat()
for (z in blocks.minPosition.z..blocks.maxPosition.z) { for (z in blocks.minPosition.z..blocks.maxPosition.z) {
@ -104,6 +108,7 @@ class SolidSectionMesher(
light[SELF_LIGHT_INDEX] = section.light[index] light[SELF_LIGHT_INDEX] = section.light[index]
position.z = offsetZ + z position.z = offsetZ + z
inSectionPosition.z = z
floatOffset[2] = (position.z - cameraOffset.z).toFloat() floatOffset[2] = (position.z - cameraOffset.z).toFloat()
val maxHeight = chunk.light.heightmap[baseIndex] val maxHeight = chunk.light.heightmap[baseIndex]
@ -136,9 +141,7 @@ class SolidSectionMesher(
floatOffset[2] += offset.z floatOffset[2] += offset.z
} }
if (ambientOcclusion) { ao?.clear()
AmbientOcclusionUtil.apply(section, x, y, z, ao)
}
val tints = tints.getBlockTint(state, chunk, x, position.y, z) val tints = tints.getBlockTint(state, chunk, x, position.y, z)

View File

@ -0,0 +1,61 @@
/*
* Minosoft
* Copyright (C) 2020-2025 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.rendering.light.ao
import de.bixilon.minosoft.data.direction.Directions
import de.bixilon.minosoft.data.world.chunk.ChunkSection
import de.bixilon.minosoft.data.world.positions.BlockPosition
import de.bixilon.minosoft.gui.rendering.light.ao.AmbientOcclusionUtil.applyBottom
import de.bixilon.minosoft.gui.rendering.light.ao.AmbientOcclusionUtil.applyEast
import de.bixilon.minosoft.gui.rendering.light.ao.AmbientOcclusionUtil.applyNorth
import de.bixilon.minosoft.gui.rendering.light.ao.AmbientOcclusionUtil.applySouth
import de.bixilon.minosoft.gui.rendering.light.ao.AmbientOcclusionUtil.applyTop
import de.bixilon.minosoft.gui.rendering.light.ao.AmbientOcclusionUtil.applyWest
class AmbientOcclusion(
val section: ChunkSection,
) {
private var input = Array(Directions.SIZE) { IntArray(AmbientOcclusionUtil.LEVELS) }
private var output = Array(Directions.SIZE) { AmbientOcclusionUtil.EMPTY }
private var dirty = 0x00
fun apply(direction: Directions, position: BlockPosition) = apply(direction, position.x, position.y, position.z)
fun apply(direction: Directions, x: Int, y: Int, z: Int): IntArray {
val mask = 1 shl direction.ordinal
if (dirty and mask != 0) return output[direction.ordinal] // already calculated
val input = input[direction.ordinal]
dirty = dirty or mask
val output = when (direction) {
Directions.DOWN -> applyBottom(section, x, y, z, input)
Directions.UP -> applyTop(section, x, y, z, input)
Directions.NORTH -> applyNorth(section, x, y, z, input)
Directions.SOUTH -> applySouth(section, x, y, z, input)
Directions.WEST -> applyWest(section, x, y, z, input)
Directions.EAST -> applyEast(section, x, y, z, input)
}
this.output[direction.ordinal] = output
return output
}
fun clear() {
dirty = 0x00
}
}

View File

@ -11,7 +11,7 @@
* This software is not affiliated with Mojang AB, the original developer of Minecraft. * This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/ */
package de.bixilon.minosoft.gui.rendering.light package de.bixilon.minosoft.gui.rendering.light.ao
import de.bixilon.minosoft.data.direction.Directions.Companion.O_DOWN import de.bixilon.minosoft.data.direction.Directions.Companion.O_DOWN
import de.bixilon.minosoft.data.direction.Directions.Companion.O_EAST import de.bixilon.minosoft.data.direction.Directions.Companion.O_EAST
@ -24,11 +24,14 @@ import de.bixilon.minosoft.data.world.chunk.ChunkSection
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
object AmbientOcclusionUtil { object AmbientOcclusionUtil {
const val LEVELS = 4
const val LEVEL_NONE = 0 const val LEVEL_NONE = 0
const val LEVEL_1 = 1 const val LEVEL_1 = 1
const val LEVEL_2 = 2 const val LEVEL_2 = 2
const val LEVEL_3 = 3 const val LEVEL_3 = 3
val EMPTY = IntArray(LEVELS) { LEVEL_NONE }
fun ChunkSection?.trace(x: Int, y: Int, z: Int): Int { fun ChunkSection?.trace(x: Int, y: Int, z: Int): Int {
if (this == null) return 0 if (this == null) return 0
var x = x var x = x
@ -37,6 +40,8 @@ object AmbientOcclusionUtil {
var section: ChunkSection? = this var section: ChunkSection? = this
if (x < -1 || x > 16 || y < -1 || y > 16 || z < -1 || z > 16) throw IllegalArgumentException("x=$x, y=$y, z=$z")
if (x < 0) { if (x < 0) {
section = section?.neighbours?.get(O_WEST) section = section?.neighbours?.get(O_WEST)
x = ProtocolDefinition.SECTION_MAX_X x = ProtocolDefinition.SECTION_MAX_X
@ -74,16 +79,9 @@ object AmbientOcclusionUtil {
return side1 + side2 + corner return side1 + side2 + corner
} }
private fun IntArray.clear() {
this[0] = LEVEL_NONE
this[1] = LEVEL_NONE
this[2] = LEVEL_NONE
this[3] = LEVEL_NONE
}
fun setY(section: ChunkSection?, x: Int, y: Int, z: Int, flip: Boolean, ao: IntArray) { fun setY(section: ChunkSection?, x: Int, y: Int, z: Int, flip: Boolean, ao: IntArray): IntArray {
ao.clear() if (section == null || section.blocks.isEmpty) return EMPTY
if (section == null || section.blocks.isEmpty) return
val west = section.trace(x - 1, y, z + 0) val west = section.trace(x - 1, y, z + 0)
val north = section.trace(x + 0, y, z - 1) val north = section.trace(x + 0, y, z - 1)
@ -94,9 +92,11 @@ object AmbientOcclusionUtil {
ao[2] = calculateLevel(east, south, section.trace(x + 1, y, z + 1)) ao[2] = calculateLevel(east, south, section.trace(x + 1, y, z + 1))
ao[if (flip) 3 else 1] = calculateLevel(north, east, section.trace(x + 1, y, z - 1)) ao[if (flip) 3 else 1] = calculateLevel(north, east, section.trace(x + 1, y, z - 1))
ao[if (flip) 1 else 3] = calculateLevel(south, west, section.trace(x - 1, y, z + 1)) ao[if (flip) 1 else 3] = calculateLevel(south, west, section.trace(x - 1, y, z + 1))
return ao
} }
fun applyBottom(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray) { fun applyBottom(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray): IntArray {
var section: ChunkSection? = section var section: ChunkSection? = section
var y = y - 1 var y = y - 1
if (y < 0) { if (y < 0) {
@ -104,10 +104,10 @@ object AmbientOcclusionUtil {
y = ProtocolDefinition.SECTION_MAX_Y y = ProtocolDefinition.SECTION_MAX_Y
} }
setY(section, x, y, z, true, ao) return setY(section, x, y, z, true, ao)
} }
fun applyTop(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray) { fun applyTop(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray): IntArray {
var section: ChunkSection? = section var section: ChunkSection? = section
var y = y + 1 var y = y + 1
if (y > ProtocolDefinition.SECTION_MAX_Y) { if (y > ProtocolDefinition.SECTION_MAX_Y) {
@ -115,12 +115,11 @@ object AmbientOcclusionUtil {
y = 0 y = 0
} }
setY(section, x, y, z, false, ao) return setY(section, x, y, z, false, ao)
} }
fun setZ(section: ChunkSection?, x: Int, y: Int, z: Int, flip: Boolean, ao: IntArray) { fun setZ(section: ChunkSection?, x: Int, y: Int, z: Int, flip: Boolean, ao: IntArray): IntArray {
ao.clear() if (section == null || section.blocks.isEmpty) return EMPTY
if (section == null || section.blocks.isEmpty) return
val down = section.trace(x + 0, y - 1, z) val down = section.trace(x + 0, y - 1, z)
val west = section.trace(x - 1, y + 0, z) val west = section.trace(x - 1, y + 0, z)
@ -131,10 +130,12 @@ object AmbientOcclusionUtil {
ao[if (flip) 3 else 1] = calculateLevel(west, up, section.trace(x - 1, y + 1, z)) ao[if (flip) 3 else 1] = calculateLevel(west, up, section.trace(x - 1, y + 1, z))
ao[2] = calculateLevel(up, east, section.trace(x + 1, y + 1, z)) ao[2] = calculateLevel(up, east, section.trace(x + 1, y + 1, z))
ao[if (flip) 1 else 3] = calculateLevel(east, down, section.trace(x + 1, y - 1, z)) ao[if (flip) 1 else 3] = calculateLevel(east, down, section.trace(x + 1, y - 1, z))
return ao
} }
fun applyNorth(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray) { fun applyNorth(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray): IntArray {
var section: ChunkSection? = section var section: ChunkSection? = section
var z = z - 1 var z = z - 1
if (z < 0) { if (z < 0) {
@ -142,10 +143,10 @@ object AmbientOcclusionUtil {
z = ProtocolDefinition.SECTION_MAX_Z z = ProtocolDefinition.SECTION_MAX_Z
} }
setZ(section, x, y, z, true, ao) return setZ(section, x, y, z, true, ao)
} }
fun applySouth(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray) { fun applySouth(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray): IntArray {
var section: ChunkSection? = section var section: ChunkSection? = section
var z = z + 1 var z = z + 1
if (z > ProtocolDefinition.SECTION_MAX_Z) { if (z > ProtocolDefinition.SECTION_MAX_Z) {
@ -153,12 +154,11 @@ object AmbientOcclusionUtil {
z = 0 z = 0
} }
setZ(section, x, y, z, false, ao) return setZ(section, x, y, z, false, ao)
} }
fun setX(section: ChunkSection?, x: Int, y: Int, z: Int, flip: Boolean, ao: IntArray) { fun setX(section: ChunkSection?, x: Int, y: Int, z: Int, flip: Boolean, ao: IntArray): IntArray {
ao.clear() if (section == null || section.blocks.isEmpty) return EMPTY
if (section == null || section.blocks.isEmpty) return
val down = section.trace(x + 0, y - 1, z) val down = section.trace(x + 0, y - 1, z)
val north = section.trace(x, y + 0, z - 1) val north = section.trace(x, y + 0, z - 1)
@ -169,9 +169,11 @@ object AmbientOcclusionUtil {
ao[if (flip) 3 else 1] = calculateLevel(north, up, section.trace(x, y + 1, z - 1)) ao[if (flip) 3 else 1] = calculateLevel(north, up, section.trace(x, y + 1, z - 1))
ao[2] = calculateLevel(up, south, section.trace(x, y + 1, z + 1)) ao[2] = calculateLevel(up, south, section.trace(x, y + 1, z + 1))
ao[if (flip) 1 else 3] = calculateLevel(south, down, section.trace(x, y - 1, z + 1)) ao[if (flip) 1 else 3] = calculateLevel(south, down, section.trace(x, y - 1, z + 1))
return ao
} }
fun applyWest(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray) { fun applyWest(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray): IntArray {
var section: ChunkSection? = section var section: ChunkSection? = section
var x = x - 1 var x = x - 1
if (x < 0) { if (x < 0) {
@ -179,10 +181,10 @@ object AmbientOcclusionUtil {
x = ProtocolDefinition.SECTION_MAX_X x = ProtocolDefinition.SECTION_MAX_X
} }
setX(section, x, y, z, false, ao) return setX(section, x, y, z, false, ao)
} }
fun applyEast(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray) { fun applyEast(section: ChunkSection, x: Int, y: Int, z: Int, ao: IntArray): IntArray {
var section: ChunkSection? = section var section: ChunkSection? = section
var x = x + 1 var x = x + 1
if (x > ProtocolDefinition.SECTION_MAX_X) { if (x > ProtocolDefinition.SECTION_MAX_X) {
@ -190,17 +192,6 @@ object AmbientOcclusionUtil {
x = 0 x = 0
} }
setX(section, x, y, z, true, ao) return setX(section, x, y, z, true, ao)
}
fun apply(section: ChunkSection, x: Int, y: Int, z: Int, ao: Array<IntArray>) {
applyBottom(section, x, y, z, ao[O_DOWN])
applyTop(section, x, y, z, ao[O_UP])
applyNorth(section, x, y, z, ao[O_NORTH])
applySouth(section, x, y, z, ao[O_SOUTH])
applyWest(section, x, y, z, ao[O_WEST])
applyEast(section, x, y, z, ao[O_EAST])
} }
} }

View File

@ -19,6 +19,7 @@ import de.bixilon.minosoft.data.direction.Directions
import de.bixilon.minosoft.data.entities.block.BlockEntity import de.bixilon.minosoft.data.entities.block.BlockEntity
import de.bixilon.minosoft.data.registries.blocks.state.BlockState import de.bixilon.minosoft.data.registries.blocks.state.BlockState
import de.bixilon.minosoft.gui.rendering.chunk.mesh.BlockVertexConsumer import de.bixilon.minosoft.gui.rendering.chunk.mesh.BlockVertexConsumer
import de.bixilon.minosoft.gui.rendering.light.ao.AmbientOcclusionUtil
import de.bixilon.minosoft.gui.rendering.models.block.state.baked.cull.FaceCulling import de.bixilon.minosoft.gui.rendering.models.block.state.baked.cull.FaceCulling
import de.bixilon.minosoft.gui.rendering.models.block.state.baked.cull.side.SideProperties import de.bixilon.minosoft.gui.rendering.models.block.state.baked.cull.side.SideProperties
import de.bixilon.minosoft.gui.rendering.models.block.state.render.BlockRender import de.bixilon.minosoft.gui.rendering.models.block.state.render.BlockRender
@ -56,16 +57,22 @@ class BakedModel(
for ((directionIndex, faces) in faces.withIndex()) { for ((directionIndex, faces) in faces.withIndex()) {
val neighbour = neighbours[directionIndex] val neighbour = neighbours[directionIndex]
val direction = Directions.VALUES[directionIndex].inverted val direction = Directions.VALUES[directionIndex]
val inverted = direction.inverted
val ao = ao[directionIndex]
for (face in faces) { for (face in faces) {
if (FaceCulling.canCull(state, face.properties, direction, neighbour)) { if (FaceCulling.canCull(state, face.properties, inverted, neighbour)) {
continue continue
} }
face.render(offset, mesh, light, tints, ao) var aoRaw = AmbientOcclusionUtil.EMPTY
if (ao != null && face.properties != null) {
aoRaw = ao.apply(direction, props.inSectionPosition)
}
face.render(offset, mesh, light, tints, aoRaw)
rendered = true rendered = true
} }

View File

@ -15,13 +15,17 @@ package de.bixilon.minosoft.gui.rendering.models.block.state.render
import de.bixilon.minosoft.data.registries.blocks.state.BlockState import de.bixilon.minosoft.data.registries.blocks.state.BlockState
import de.bixilon.minosoft.data.world.positions.BlockPosition import de.bixilon.minosoft.data.world.positions.BlockPosition
import de.bixilon.minosoft.data.world.positions.InChunkSectionPosition
import de.bixilon.minosoft.gui.rendering.chunk.mesh.BlockVertexConsumer import de.bixilon.minosoft.gui.rendering.chunk.mesh.BlockVertexConsumer
import de.bixilon.minosoft.gui.rendering.light.ao.AmbientOcclusion
import java.util.* import java.util.*
class WorldRenderProps( class WorldRenderProps(
@JvmField @JvmField
val position: BlockPosition, val position: BlockPosition,
@JvmField @JvmField
val inSectionPosition: InChunkSectionPosition,
@JvmField
val offset: FloatArray, val offset: FloatArray,
@JvmField @JvmField
val mesh: BlockVertexConsumer, val mesh: BlockVertexConsumer,
@ -32,5 +36,5 @@ class WorldRenderProps(
@JvmField @JvmField
val light: ByteArray, val light: ByteArray,
@JvmField @JvmField
var ao: Array<IntArray>, val ao: AmbientOcclusion?,
) )