package mods.betterfoliage.client.render import mods.betterfoliage.client.config.BlockMatcher import mods.betterfoliage.client.integration.OptifineCTM import mods.betterfoliage.client.render.AbstractRenderColumn.BlockType.* import mods.betterfoliage.client.render.AbstractRenderColumn.QuadrantType.* import mods.octarinecore.client.render.* import mods.octarinecore.client.resource.BlockTextureInspector import mods.octarinecore.common.* import net.minecraft.block.state.IBlockState import net.minecraft.client.renderer.BlockRendererDispatcher import net.minecraft.client.renderer.WorldRenderer import net.minecraft.client.renderer.texture.TextureAtlasSprite import net.minecraft.client.renderer.texture.TextureMap import net.minecraft.util.EnumFacing import net.minecraft.util.EnumFacing.* import net.minecraft.util.EnumWorldBlockLayer interface ColumnTextureResolver { val top: (ShadingContext, Int, Quad)->TextureAtlasSprite? val bottom: (ShadingContext, Int, Quad)->TextureAtlasSprite? val side: (ShadingContext, Int, Quad)->TextureAtlasSprite? } data class StaticColumnInfo(val topTexture: TextureAtlasSprite, val bottomTexture: TextureAtlasSprite, val sideTexture: TextureAtlasSprite) : ColumnTextureResolver { override val top = { ctx: ShadingContext, idx: Int, quad: Quad -> OptifineCTM.override(topTexture, blockContext, UP.rotate(ctx.rotation)) } override val bottom = { ctx: ShadingContext, idx: Int, quad: Quad -> OptifineCTM.override(bottomTexture, blockContext, DOWN.rotate(ctx.rotation)) } override val side = { ctx: ShadingContext, idx: Int, quad: Quad -> OptifineCTM.override(sideTexture, blockContext, (if ((idx and 1) == 0) SOUTH else EAST).rotate(ctx.rotation)) } } open class ColumnTextures(val matcher: BlockMatcher) : BlockTextureInspector() { init { matchClassAndModel(matcher, "block/column_side", listOf("end", "end", "side")) matchClassAndModel(matcher, "block/cube_column", listOf("end", "end", "side")) matchClassAndModel(matcher, "block/cube_all", listOf("all", "all", "all")) } override fun processTextures(state: IBlockState, textures: List, atlas: TextureMap) = StaticColumnInfo(textures[0], textures[1], textures[2]) } /** Index of SOUTH-EAST quadrant. */ const val SE = 0 /** Index of NORTH-EAST quadrant. */ const val NE = 1 /** Index of NORTH-WEST quadrant. */ const val NW = 2 /** Index of SOUTH-WEST quadrant. */ const val SW = 3 @Suppress("NOTHING_TO_INLINE") abstract class AbstractRenderColumn(modId: String) : AbstractBlockRenderingHandler(modId) { enum class BlockType { SOLID, NONSOLID, PARALLEL, PERPENDICULAR } enum class QuadrantType { SMALL_RADIUS, LARGE_RADIUS, SQUARE, INVISIBLE } /** The rotations necessary to bring the models in position for the 4 quadrants */ val quadrantRotations = Array(4) { Rotation.rot90[UP.ordinal] * it } // ============================ // Configuration // ============================ abstract val radiusSmall: Double abstract val radiusLarge: Double abstract val surroundPredicate: (IBlockState) -> Boolean abstract val connectPerpendicular: Boolean abstract val connectSolids: Boolean abstract val lenientConnect: Boolean // ============================ // Models // ============================ val sideSquare = model { columnSideSquare(-0.5, 0.5) } val sideRoundSmall = model { columnSide(radiusSmall, -0.5, 0.5) } val sideRoundLarge = model { columnSide(radiusLarge, -0.5, 0.5) } val extendTopSquare = model { columnSideSquare(0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) } val extendTopRoundSmall = model { columnSide(radiusSmall, 0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) } val extendTopRoundLarge = model { columnSide(radiusLarge, 0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) } inline fun extendTop(type: QuadrantType) = when(type) { SMALL_RADIUS -> extendTopRoundSmall.model LARGE_RADIUS -> extendTopRoundLarge.model SQUARE -> extendTopSquare.model INVISIBLE -> extendTopSquare.model else -> null } val extendBottomSquare = model { columnSideSquare(-0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) } val extendBottomRoundSmall = model { columnSide(radiusSmall, -0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) } val extendBottomRoundLarge = model { columnSide(radiusLarge, -0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) } inline fun extendBottom(type: QuadrantType) = when (type) { SMALL_RADIUS -> extendBottomRoundSmall.model LARGE_RADIUS -> extendBottomRoundLarge.model SQUARE -> extendBottomSquare.model INVISIBLE -> extendBottomSquare.model else -> null } val topSquare = model { columnLidSquare() } val topRoundSmall = model { columnLid(radiusSmall) } val topRoundLarge = model { columnLid(radiusLarge) } inline fun flatTop(type: QuadrantType) = when(type) { SMALL_RADIUS -> topRoundSmall.model LARGE_RADIUS -> topRoundLarge.model SQUARE -> topSquare.model INVISIBLE -> topSquare.model else -> null } val bottomSquare = model { columnLidSquare() { it.rotate(rot(EAST) * 2 + rot(UP)) } } val bottomRoundSmall = model { columnLid(radiusSmall) { it.rotate(rot(EAST) * 2 + rot(UP)) } } val bottomRoundLarge = model { columnLid(radiusLarge) { it.rotate(rot(EAST) * 2 + rot(UP)) } } inline fun flatBottom(type: QuadrantType) = when(type) { SMALL_RADIUS -> bottomRoundSmall.model LARGE_RADIUS -> bottomRoundLarge.model SQUARE -> bottomSquare.model INVISIBLE -> bottomSquare.model else -> null } val transitionTop = model { mix(sideRoundLarge.model, sideRoundSmall.model) { it > 1 } } val transitionBottom = model { mix(sideRoundSmall.model, sideRoundLarge.model) { it > 1 } } inline fun continous(q1: QuadrantType, q2: QuadrantType) = q1 == q2 || ((q1 == SQUARE || q1 == INVISIBLE) && (q2 == SQUARE || q2 == INVISIBLE)) abstract val axisFunc: (IBlockState)->EnumFacing.Axis? abstract val blockPredicate: (IBlockState)->Boolean abstract fun resolver(ctx: BlockContext): ColumnTextureResolver? @Suppress("NON_EXHAUSTIVE_WHEN") override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: WorldRenderer, layer: EnumWorldBlockLayer): Boolean { if (ctx.isSurroundedBy(surroundPredicate) ) return false val columnTextures = resolver(ctx) ?: return false // get AO data modelRenderer.updateShading(Int3.zero, allFaces) // check log neighborhood val logAxis = ctx.blockAxis ?: return renderWorldBlockBase(ctx, dispatcher, renderer, null) val baseRotation = rotationFromUp[(logAxis to AxisDirection.POSITIVE).face.ordinal] val upType = ctx.blockType(baseRotation, logAxis, Int3(0, 1, 0)) val downType = ctx.blockType(baseRotation, logAxis, Int3(0, -1, 0)) val quadrants = Array(4) { SMALL_RADIUS }.checkNeighbors(ctx, baseRotation, logAxis, 0) val quadrantsTop = Array(4) { SMALL_RADIUS } if (upType == PARALLEL) quadrantsTop.checkNeighbors(ctx, baseRotation, logAxis, 1) val quadrantsBottom = Array(4) { SMALL_RADIUS } if (downType == PARALLEL) quadrantsBottom.checkNeighbors(ctx, baseRotation, logAxis, -1) quadrantRotations.forEachIndexed { idx, quadrantRotation -> // set rotation for the current quadrant val rotation = baseRotation + quadrantRotation // disallow sharp discontinuities in the chamfer radius, or tapering-in where inappropriate if (quadrants[idx] == LARGE_RADIUS && upType == PARALLEL && quadrantsTop[idx] != LARGE_RADIUS && downType == PARALLEL && quadrantsBottom[idx] != LARGE_RADIUS) { quadrants[idx] = SMALL_RADIUS } // render side of current quadrant val sideModel = when (quadrants[idx]) { SMALL_RADIUS -> sideRoundSmall.model LARGE_RADIUS -> if (upType == PARALLEL && quadrantsTop[idx] == SMALL_RADIUS) transitionTop.model else if (downType == PARALLEL && quadrantsBottom[idx] == SMALL_RADIUS) transitionBottom.model else sideRoundLarge.model SQUARE -> sideSquare.model else -> null } if (sideModel != null) modelRenderer.render( renderer, sideModel, rotation, blockContext.blockCenter, icon = columnTextures.side, rotateUV = { 0 }, postProcess = noPost ) // render top and bottom end of current quadrant var upModel: Model? = null var downModel: Model? = null var upIcon = columnTextures.top var downIcon = columnTextures.bottom var shouldRotateUp = true var shouldRotateDown = true when (upType) { NONSOLID -> upModel = flatTop(quadrants[idx]) PERPENDICULAR -> { if (!connectPerpendicular) { upModel = flatTop(quadrants[idx]) } else { upIcon = columnTextures.side upModel = extendTop(quadrants[idx]) shouldRotateUp = false } } PARALLEL -> { if (!continous(quadrants[idx], quadrantsTop[idx])) { if (quadrants[idx] == SQUARE || quadrants[idx] == INVISIBLE) { upModel = topSquare.model } } } } when (downType) { NONSOLID -> downModel = flatBottom(quadrants[idx]) PERPENDICULAR -> { if (!connectPerpendicular) { downModel = flatBottom(quadrants[idx]) } else { downIcon = columnTextures.side downModel = extendBottom(quadrants[idx]) shouldRotateDown = false } } PARALLEL -> { if (!continous(quadrants[idx], quadrantsBottom[idx]) && (quadrants[idx] == SQUARE || quadrants[idx] == INVISIBLE)) { downModel = bottomSquare.model } } } if (upModel != null) modelRenderer.render( renderer, upModel, rotation, blockContext.blockCenter, icon = upIcon, rotateUV = { if (shouldRotateUp) idx else 0 }, postProcess = noPost ) if (downModel != null) modelRenderer.render( renderer, downModel, rotation, blockContext.blockCenter, icon = downIcon, rotateUV = { if (shouldRotateDown) 3 - idx else 0 }, postProcess = noPost ) } return true } /** Sets the type of the given quadrant only if the new value is "stronger" (larger ordinal). */ inline fun Array.upgrade(idx: Int, value: QuadrantType) { if (this[idx].ordinal < value.ordinal) this[idx] = value } /** Fill the array of [QuadrantType]s based on the blocks to the sides of this one. */ fun Array.checkNeighbors(ctx: BlockContext, rotation: Rotation, logAxis: Axis, yOff: Int): Array { val blkS = ctx.blockType(rotation, logAxis, Int3(0, yOff, 1)) val blkE = ctx.blockType(rotation, logAxis, Int3(1, yOff, 0)) val blkN = ctx.blockType(rotation, logAxis, Int3(0, yOff, -1)) val blkW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, 0)) // a solid block on one side will make the 2 neighboring quadrants SQUARE // if there are solid blocks to both sides of a quadrant, it is INVISIBLE if (connectSolids) { if (blkS == SOLID) { upgrade(SW, SQUARE); upgrade(SE, SQUARE) } if (blkE == SOLID) { upgrade(SE, SQUARE); upgrade(NE, SQUARE) } if (blkN == SOLID) { upgrade(NE, SQUARE); upgrade(NW, SQUARE) } if (blkW == SOLID) { upgrade(NW, SQUARE); upgrade(SW, SQUARE) } if (blkS == SOLID && blkE == SOLID) upgrade(SE, INVISIBLE) if (blkN == SOLID && blkE == SOLID) upgrade(NE, INVISIBLE) if (blkN == SOLID && blkW == SOLID) upgrade(NW, INVISIBLE) if (blkS == SOLID && blkW == SOLID) upgrade(SW, INVISIBLE) } val blkSE = ctx.blockType(rotation, logAxis, Int3(1, yOff, 1)) val blkNE = ctx.blockType(rotation, logAxis, Int3(1, yOff, -1)) val blkNW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, -1)) val blkSW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, 1)) if (lenientConnect) { // if the block forms the tip of an L-shape, connect to its neighbor with SQUARE quadrants if (blkE == PARALLEL && (blkSE == PARALLEL || blkNE == PARALLEL)) { upgrade(SE, SQUARE); upgrade(NE, SQUARE) } if (blkN == PARALLEL && (blkNE == PARALLEL || blkNW == PARALLEL)) { upgrade(NE, SQUARE); upgrade(NW, SQUARE) } if (blkW == PARALLEL && (blkNW == PARALLEL || blkSW == PARALLEL)) { upgrade(NW, SQUARE); upgrade(SW, SQUARE) } if (blkS == PARALLEL && (blkSE == PARALLEL || blkSW == PARALLEL)) { upgrade(SW, SQUARE); upgrade(SE, SQUARE) } } // if the block forms the middle of an L-shape, or is part of a 2x2 configuration, // connect to its neighbors with SQUARE quadrants, INVISIBLE on the inner corner, and LARGE_RADIUS on the outer corner if (blkN == PARALLEL && blkW == PARALLEL && (lenientConnect || blkNW == PARALLEL)) { upgrade(SE, LARGE_RADIUS); upgrade(NE, SQUARE); upgrade(SW, SQUARE); upgrade(NW, INVISIBLE) } if (blkS == PARALLEL && blkW == PARALLEL && (lenientConnect || blkSW == PARALLEL)) { upgrade(NE, LARGE_RADIUS); upgrade(SE, SQUARE); upgrade(NW, SQUARE); upgrade(SW, INVISIBLE) } if (blkS == PARALLEL && blkE == PARALLEL && (lenientConnect || blkSE == PARALLEL)) { upgrade(NW, LARGE_RADIUS); upgrade(NE, SQUARE); upgrade(SW, SQUARE); upgrade(SE, INVISIBLE) } if (blkN == PARALLEL && blkE == PARALLEL && (lenientConnect || blkNE == PARALLEL)) { upgrade(SW, LARGE_RADIUS); upgrade(SE, SQUARE); upgrade(NW, SQUARE); upgrade(NE, INVISIBLE) } return this } /** Get the axis of the block */ val BlockContext.blockAxis: Axis? get() = axisFunc(blockState(Int3.zero)) /** * Get the type of the block at the given offset in a rotated reference frame. */ fun BlockContext.blockType(rotation: Rotation, axis: Axis, offset: Int3): BlockType { val offsetRot = offset.rotate(rotation) val state = blockState(offsetRot) return if (!blockPredicate(state)) { if (state.block.isOpaqueCube) SOLID else NONSOLID } else { axisFunc(state)?.let { if (it == axis) PARALLEL else PERPENDICULAR } ?: SOLID } } }