[WIP] major rewrite, grass and leaves working already

This commit is contained in:
octarine-noise
2021-05-06 22:40:32 +02:00
parent 09ccb83e8b
commit f44d2a7a50
96 changed files with 2348 additions and 3531 deletions

View File

@@ -0,0 +1,138 @@
package mods.betterfoliage.render.lighting
import mods.betterfoliage.util.get
import mods.betterfoliage.util.mapArray
import mods.betterfoliage.util.perpendiculars
import net.minecraft.util.Direction
import net.minecraft.util.Direction.*
typealias BoxCorner = Triple<Direction, Direction, Direction>
fun BoxCorner.equalsUnordered(other: BoxCorner) = contains(other.first) && contains(other.second) && contains(other.third)
fun BoxCorner.contains(dir: Direction) = first == dir || second == dir || third == dir
fun Array<BoxCorner>.findIdx(corner: BoxCorner): Int? {
forEachIndexed { idx, test -> if (test.contains(corner.first) && test.contains(corner.second) && test.contains(corner.third)) return idx }
return null
}
fun Array<BoxCorner>.findIdx(predicate: (BoxCorner)->Boolean): Int? {
forEachIndexed { idx, test -> if (predicate(test)) return idx }
return null
}
class AoSideHelper private constructor(face: Direction) {
val sides = faceSides[face]
val cornerSideDirections = faceCorners[face]
val aoIndex = faceCornersIdx.mapArray { corner ->
boxCornersDirIdx[face][sides[corner.first]][sides[corner.second]]!!
}
companion object {
/**
* Indexing for undirected box corners (component order does not matter).
* Array contains [Direction] triplets fully defining the corner.
*/
@JvmField
val boxCornersUndir = Array(8) { idx -> Triple(
if (idx and 1 != 0) EAST else WEST,
if (idx and 2 != 0) UP else DOWN,
if (idx and 4 != 0) SOUTH else NORTH
) }
/**
* Reverse lookup for [boxCornersUndir]. Index 3 times with the corner's cardinal directions.
* A null value indicates an invalid corner (multiple indexing along the same axis)
*/
@JvmField
val boxCornersUndirIdx = Array(6) { idx1 -> Array(6) { idx2 -> Array(6) { idx3 ->
boxCornersUndir.findIdx(BoxCorner(
Direction.values()[idx1],
Direction.values()[idx2],
Direction.values()[idx3]
))
} } }
/**
* Indexing for directed face sides
* First index is the face, second is index of side on face
*/
@JvmField
val faceSides = Array(6) { faceIdx -> Array(4) { sideIdx ->
Direction.values()[faceIdx].perpendiculars[sideIdx]
} }
/**
* Pairs of [faceSides] side indexes that form a valid pair describing a corner
*/
@JvmField
val faceCornersIdx = arrayOf(0 to 2, 0 to 3, 1 to 2, 1 to 3)
/**
* Indexing for directed face corners
* First index is the face, second is index of corner on face
*/
@JvmField
val faceCorners = Array(6) { faceIdx -> Array(4) { cornerIdx ->
faceCornersIdx[cornerIdx].let { faceSides[faceIdx][it.first] to faceSides[faceIdx][it.second] }
} }
/**
* Indexing scheme for directed box corners.
* The first direction - the face - matters, the other two are unordered.
* 1:1 correspondence with possible AO values.
* Array contains triplets defining the corner fully.
*/
@JvmField
val boxCornersDir = Array(24) { idx ->
val faceIdx = idx / 4; val face = Direction.values()[faceIdx]
val cornerIdx = idx % 4; val corner = faceCorners[faceIdx][cornerIdx]
BoxCorner(face, corner.first, corner.second)
}
/**
* Reverse lookup for [boxCornersDir]. Index 3 times with the corner's cardinal directions.
* The first direction - the face - matters, the other two are unordered.
* A null value indicates an invalid corner (multiple indexing along the same axis)
*/
@JvmField
val boxCornersDirIdx = Array(6) { face -> Array(6) { side1 -> Array(6) { side2 ->
boxCornersDir.findIdx { boxCorner ->
boxCorner.first.ordinal == face && boxCorner.equalsUnordered(BoxCorner(
Direction.values()[face],
Direction.values()[side1],
Direction.values()[side2]
))
}
} } }
/**
* Reverse lookup for [cornersDir].
* 1st index: primary face
* 2nd index: undirected corner index.
* value: directed corner index
* A null value indicates an invalid corner (primary face not shared by corner).
*/
@JvmField
val boxCornersDirFromUndir = Array(6) { faceIdx -> Array(8) { undirIdx ->
val face = Direction.values()[faceIdx]
val corner = boxCornersUndir[undirIdx]
if (!corner.contains(face)) null
else boxCornersDir.findIdx { it.first == face && it.equalsUnordered(corner) }
} }
@JvmField
val forSide = Direction.values().mapArray { AoSideHelper(it) }
/**
* Get corner index for vertex coordinates
*/
@JvmStatic
fun getCornerUndir(x: Double, y: Double, z: Double): Int {
var result = 0
if (x > 0.0) result += 1
if (y > 0.0) result += 2
if (z > 0.0) result += 4
return result
}
}
}

View File

@@ -0,0 +1,33 @@
package mods.betterfoliage.render.lighting
interface ForgeVertexLighterAccess {
var vertexLighter: ForgeVertexLighter
}
interface ForgeVertexLighter {
fun updateVertexLightmap(normal: FloatArray, lightmap: FloatArray, x: Float, y: Float, z: Float)
fun updateVertexColor(normal: FloatArray, color: FloatArray, x: Float, y: Float, z: Float, tint: Float, multiplier: Int)
}
fun ForgeVertexLighter.grass() = object: ForgeVertexLighter {
override fun updateVertexLightmap(normal: FloatArray, lightmap: FloatArray, x: Float, y: Float, z: Float) {
this@grass.updateVertexLightmap(normal, lightmap, x * 0.5f, 1.0f, z * 0.5f)
}
override fun updateVertexColor(normal: FloatArray, color: FloatArray, x: Float, y: Float, z: Float, tint: Float, multiplier: Int) {
this@grass.updateVertexColor(normal, color, x * 0.5f, 1.0f, z * 0.5f, tint, multiplier
)
}
}
fun ForgeVertexLighter.grassSimple() = object: ForgeVertexLighter {
val normalUp = floatArrayOf(0.0f, 1.0f, 0.0f, 0.0f)
override fun updateVertexLightmap(normal: FloatArray, lightmap: FloatArray, x: Float, y: Float, z: Float) {
this@grassSimple.updateVertexLightmap(normalUp, lightmap, 0.0f, 1.0f, 0.0f)
}
override fun updateVertexColor(normal: FloatArray, color: FloatArray, x: Float, y: Float, z: Float, tint: Float, multiplier: Int) {
this@grassSimple.updateVertexColor(normalUp, color, 0.0f, 1.0f, 0.0f, tint, multiplier
)
}
}

View File

@@ -1,158 +0,0 @@
package mods.betterfoliage.render.lighting
import mods.betterfoliage.render.old.Quad
import mods.betterfoliage.render.old.Vertex
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.Rotation
import mods.betterfoliage.util.axes
import mods.betterfoliage.util.boxEdges
import mods.betterfoliage.util.boxFaces
import mods.betterfoliage.util.face
import mods.betterfoliage.util.get
import mods.betterfoliage.util.nearestAngle
import mods.betterfoliage.util.nearestPosition
import mods.betterfoliage.util.perpendiculars
import mods.betterfoliage.util.vec
import net.minecraft.util.Direction
import net.minecraft.util.Direction.*
import java.lang.Math.min
typealias EdgeShaderFactory = (Direction, Direction) -> ModelLighter
typealias CornerShaderFactory = (Direction, Direction, Direction) -> ModelLighter
typealias ShaderFactory = (Quad, Vertex) -> ModelLighter
/** Holds lighting values for block corners as calculated by vanilla Minecraft rendering. */
class CornerLightData {
var valid = false
var brightness = 0
var red: Float = 0.0f
var green: Float = 0.0f
var blue: Float = 0.0f
fun reset() { valid = false }
fun set(brightness: Int, red: Float, green: Float, blue: Float) {
if (valid) return
this.valid = true
this.brightness = brightness
this.red = red
this.green = green
this.blue = blue
}
fun set(brightness: Int, colorMultiplier: Float) {
this.valid = true
this.brightness = brightness
this.red = colorMultiplier
this.green = colorMultiplier
this.blue = colorMultiplier
}
companion object {
val black: CornerLightData get() = CornerLightData()
}
}
/**
* Instances of this interface are associated with [Model] vertices, and used to apply brightness and color
* values to a [RenderVertex].
*/
interface ModelLighter {
/**
* Set shading values of a [RenderVertex]
*
* @param[context] context that can be queried for lighting data in a [Model]-relative frame of reference
* @param[vertex] the [RenderVertex] to manipulate
*/
fun shade(context: LightingCtx, vertex: RenderVertex)
/**
* Return a new rotated version of this [ModelLighter]. Used during [Model] setup when rotating the model itself.
*/
fun rotate(rot: Rotation): ModelLighter
/** Set all lighting values on the [RenderVertex] to match the given [CornerLightData]. */
fun RenderVertex.shade(shading: CornerLightData) {
brightness = shading.brightness; red = shading.red; green = shading.green; blue = shading.blue
}
/** Set the lighting values on the [RenderVertex] to a weighted average of the two [CornerLightData] instances. */
fun RenderVertex.shade(shading1: CornerLightData, shading2: CornerLightData, weight1: Float = 0.5f, weight2: Float = 0.5f) {
red = min(shading1.red * weight1 + shading2.red * weight2, 1.0f)
green = min(shading1.green * weight1 + shading2.green * weight2, 1.0f)
blue = min(shading1.blue * weight1 + shading2.blue * weight2, 1.0f)
brightness = brWeighted(shading1.brightness, weight1, shading2.brightness, weight2)
}
/**
* Set the lighting values on the [RenderVertex] directly.
*
* @param[brightness] packed brightness value
* @param[color] packed color value
*/
fun RenderVertex.shade(brightness: Int, color: Int) {
this.brightness = brightness; setColor(color)
}
}
/**
* Returns a [ModelLighter] resolver for quads that point towards one of the 6 block faces.
* The resolver works the following way:
* - determines which face the _quad_ normal points towards (if not overridden)
* - determines the distance of the _vertex_ to the corners and edge midpoints on that block face
* - if _corner_ is given, and the _vertex_ is closest to a block corner, returns the [ModelLighter] created by _corner_
* - if _edge_ is given, and the _vertex_ is closest to an edge midpoint, returns the [ModelLighter] created by _edge_
*
* @param[overrideFace] assume the given face instead of going by the _quad_ normal
* @param[corner] [ModelLighter] instantiation lambda for corner vertices
* @param[edge] [ModelLighter] instantiation lambda for edge midpoint vertices
*/
fun faceOrientedAuto(overrideFace: Direction? = null,
corner: CornerShaderFactory? = null,
edge: EdgeShaderFactory? = null) =
fun(quad: Quad, vertex: Vertex): ModelLighter {
val quadFace = overrideFace ?: quad.normal.nearestCardinal
val nearestCorner = nearestPosition(vertex.xyz, boxFaces[quadFace].allCorners) {
(quadFace.vec + it.first.vec + it.second.vec) * 0.5
}
val nearestEdge = nearestPosition(vertex.xyz, quadFace.perpendiculars) {
(quadFace.vec + it.vec) * 0.5
}
if (edge != null && (nearestEdge.second < nearestCorner.second || corner == null))
return edge(quadFace, nearestEdge.first)
else return corner!!(quadFace, nearestCorner.first.first, nearestCorner.first.second)
}
/**
* Returns a ModelLighter resolver for quads that point towards one of the 12 block edges.
* The resolver works the following way:
* - determines which edge the _quad_ normal points towards (if not overridden)
* - determines which face midpoint the _vertex_ is closest to, of the 2 block faces that share this edge
* - determines which block corner _of this face_ the _vertex_ is closest to
* - returns the [ModelLighter] created by _corner_
*
* @param[overrideEdge] assume the given edge instead of going by the _quad_ normal
* @param[corner] ModelLighter instantiation lambda
*/
fun edgeOrientedAuto(overrideEdge: Pair<Direction, Direction>? = null,
corner: CornerShaderFactory
) =
fun(quad: Quad, vertex: Vertex): ModelLighter {
val edgeDir = overrideEdge ?: nearestAngle(quad.normal, boxEdges) { it.first.vec + it.second.vec }.first
val nearestFace = nearestPosition(vertex.xyz, edgeDir.toList()) { it.vec }.first
val nearestCorner = nearestPosition(vertex.xyz, boxFaces[nearestFace].allCorners) {
(nearestFace.vec + it.first.vec + it.second.vec) * 0.5
}.first
return corner(nearestFace, nearestCorner.first, nearestCorner.second)
}
fun faceOrientedInterpolate(overrideFace: Direction? = null) =
fun(quad: Quad, vertex: Vertex): ModelLighter {
val resolver = faceOrientedAuto(overrideFace, edge = { face, edgeDir ->
val axis = axes.find { it != face.axis && it != edgeDir.axis }!!
val vec = Double3((axis to AxisDirection.POSITIVE).face)
val pos = vertex.xyz.dot(vec)
EdgeInterpolateFallback(face, edgeDir, pos)
})
return resolver(quad, vertex)
}

View File

@@ -1,118 +0,0 @@
package mods.betterfoliage.render.lighting
import mods.betterfoliage.render.old.BlockCtx
import mods.betterfoliage.util.Int3
import mods.betterfoliage.util.Rotation
import mods.betterfoliage.util.allDirections
import mods.betterfoliage.util.boxFaces
import mods.betterfoliage.util.get
import mods.betterfoliage.util.offset
import mods.betterfoliage.util.plus
import mods.betterfoliage.util.rotate
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.BlockModelRenderer
import net.minecraft.client.renderer.WorldRenderer
import net.minecraft.util.Direction
import java.util.*
val Direction.aoMultiplier: Float get() = when(this) {
Direction.UP -> 1.0f
Direction.DOWN -> 0.5f
Direction.NORTH, Direction.SOUTH -> 0.8f
Direction.EAST, Direction.WEST -> 0.6f
}
interface LightingCtx {
val modelRotation: Rotation
val blockContext: BlockCtx
val aoEnabled: Boolean
val brightness get() = brightness(Int3.zero)
val color get() = color(Int3.zero)
fun brightness(face: Direction) = brightness(face.offset)
fun color(face: Direction) = color(face.offset)
fun brightness(offset: Int3) = offset.rotate(modelRotation).let {
WorldRenderer.getCombinedLight(blockContext.world, blockContext.pos + it)
}
fun color(offset: Int3) = blockContext.offset(offset.rotate(modelRotation)).let { Minecraft.getInstance().blockColors.getColor(it.state, it.world, it.pos, 0) }
fun lighting(face: Direction, corner1: Direction, corner2: Direction): CornerLightData
}
class DefaultLightingCtx(blockContext: BlockCtx) : LightingCtx {
override var modelRotation = Rotation.identity
override var aoEnabled = false
protected set
override var blockContext: BlockCtx = blockContext
protected set
override var brightness = brightness(Int3.zero)
protected set
override var color = color(Int3.zero)
protected set
override fun brightness(face: Direction) = brightness(face.offset)
override fun color(face: Direction) = color(face.offset)
// smooth lighting stuff
val lightingData = Array(6) { FaceLightData(allDirections[it]) }
override fun lighting(face: Direction, corner1: Direction, corner2: Direction): CornerLightData = lightingData[face.rotate(modelRotation)].let { faceData ->
if (!faceData.isValid) faceData.update(blockContext, faceData.face.aoMultiplier)
return faceData[corner1.rotate(modelRotation), corner2.rotate(modelRotation)]
}
fun reset(blockContext: BlockCtx) {
this.blockContext = blockContext
brightness = brightness(Int3.zero)
color = color(Int3.zero)
modelRotation = Rotation.identity
lightingData.forEach { it.isValid = false }
aoEnabled = Minecraft.isAmbientOcclusionEnabled()
// allDirections.forEach { lightingData[it].update(blockContext, it.aoMultiplier) }
}
}
private val vanillaAOFactory = BlockModelRenderer.AmbientOcclusionFace::class.java.let {
it.getDeclaredConstructor(BlockModelRenderer::class.java).apply { isAccessible = true }
}.let { ctor -> { ctor.newInstance(Minecraft.getInstance().blockRendererDispatcher.blockModelRenderer) } }
class FaceLightData(val face: Direction) {
val topDir = boxFaces[face].top
val leftDir = boxFaces[face].left
val topLeft = CornerLightData()
val topRight = CornerLightData()
val bottomLeft = CornerLightData()
val bottomRight = CornerLightData()
val vanillaOrdered = when(face) {
Direction.DOWN -> listOf(topLeft, bottomLeft, bottomRight, topRight)
Direction.UP -> listOf(bottomRight, topRight, topLeft, bottomLeft)
Direction.NORTH -> listOf(bottomLeft, bottomRight, topRight, topLeft)
Direction.SOUTH -> listOf(topLeft, bottomLeft, bottomRight, topRight)
Direction.WEST -> listOf(bottomLeft, bottomRight, topRight, topLeft)
Direction.EAST -> listOf(topRight, topLeft, bottomLeft, bottomRight)
}
val delegate = vanillaAOFactory()
var isValid = false
fun update(blockCtx: BlockCtx, multiplier: Float) {
val quadBounds = FloatArray(12)
val flags = BitSet(3).apply { set(0) }
// delegate.updateVertexBrightness(blockCtx.world, blockCtx.state, blockCtx.pos, face, quadBounds, flags)
vanillaOrdered.forEachIndexed { idx, corner -> corner.set(delegate.vertexBrightness[idx], delegate.vertexColorMultiplier[idx] * multiplier) }
isValid = true
}
operator fun get(dir1: Direction, dir2: Direction): CornerLightData {
val isTop = topDir == dir1 || topDir == dir2
val isLeft = leftDir == dir1 || leftDir == dir2
return if (isTop) {
if (isLeft) topLeft else topRight
} else {
if (isLeft) bottomLeft else bottomRight
}
}
}

View File

@@ -1,162 +0,0 @@
package mods.betterfoliage.render.lighting
import mods.betterfoliage.util.Int3
import mods.betterfoliage.util.Rotation
import mods.betterfoliage.util.axes
import mods.betterfoliage.util.boxFaces
import mods.betterfoliage.util.face
import mods.betterfoliage.util.get
import mods.betterfoliage.util.offset
import mods.betterfoliage.util.rotate
import net.minecraft.util.Direction
const val defaultCornerDimming = 0.5f
const val defaultEdgeDimming = 0.8f
// ================================
// Shader instantiation lambdas
// ================================
fun cornerAo(fallbackAxis: Direction.Axis): CornerShaderFactory = { face, dir1, dir2 ->
val fallbackDir = listOf(face, dir1, dir2).find { it.axis == fallbackAxis }!!
CornerSingleFallback(face, dir1, dir2, fallbackDir)
}
val cornerFlat = { face: Direction, dir1: Direction, dir2: Direction -> FaceFlat(face) }
fun cornerAoTri(func: (CornerLightData, CornerLightData)-> CornerLightData) = { face: Direction, dir1: Direction, dir2: Direction ->
CornerTri(face, dir1, dir2, func)
}
val cornerAoMaxGreen = cornerAoTri { s1, s2 -> if (s1.green > s2.green) s1 else s2 }
fun cornerInterpolate(edgeAxis: Direction.Axis, weight: Float, dimming: Float): CornerShaderFactory = { dir1, dir2, dir3 ->
val edgeDir = listOf(dir1, dir2, dir3).find { it.axis == edgeAxis }!!
val faceDirs = listOf(dir1, dir2, dir3).filter { it.axis != edgeAxis }
CornerInterpolateDimming(faceDirs[0], faceDirs[1], edgeDir, weight, dimming)
}
// ================================
// Shaders
// ================================
object NoLighting : ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) = vertex.shade(CornerLightData.black)
override fun rotate(rot: Rotation) = this
}
class CornerSingleFallback(val face: Direction, val dir1: Direction, val dir2: Direction, val fallbackDir: Direction, val fallbackDimming: Float = defaultCornerDimming) :
ModelLighter {
val offset = Int3(fallbackDir)
override fun shade(context: LightingCtx, vertex: RenderVertex) {
val shading = context.lighting(face, dir1, dir2)
if (shading.valid)
vertex.shade(shading)
else {
vertex.shade(context.brightness(offset) brMul fallbackDimming, context.color(offset) colorMul fallbackDimming)
}
}
override fun rotate(rot: Rotation) = CornerSingleFallback(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), fallbackDir.rotate(rot), fallbackDimming)
}
inline fun accumulate(v1: CornerLightData?, v2: CornerLightData?, func: ((CornerLightData, CornerLightData)-> CornerLightData)): CornerLightData? {
val v1ok = v1 != null && v1.valid
val v2ok = v2 != null && v2.valid
if (v1ok && v2ok) return func(v1!!, v2!!)
if (v1ok) return v1
if (v2ok) return v2
return null
}
class CornerTri(val face: Direction, val dir1: Direction, val dir2: Direction,
val func: ((CornerLightData, CornerLightData)-> CornerLightData)) : ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) {
var acc = accumulate(
context.lighting(face, dir1, dir2),
context.lighting(dir1, face, dir2),
func)
acc = accumulate(
acc,
context.lighting(dir2, face, dir1),
func)
vertex.shade(acc ?: CornerLightData.black)
}
override fun rotate(rot: Rotation) = CornerTri(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), func)
}
class EdgeInterpolateFallback(val face: Direction, val edgeDir: Direction, val pos: Double, val fallbackDimming: Float = defaultEdgeDimming):
ModelLighter {
val offset = Int3(edgeDir)
val edgeAxis = axes.find { it != face.axis && it != edgeDir.axis }!!
val weightN = (0.5 - pos).toFloat()
val weightP = (0.5 + pos).toFloat()
override fun shade(context: LightingCtx, vertex: RenderVertex) {
val shadingP = context.lighting(face, edgeDir, (edgeAxis to Direction.AxisDirection.POSITIVE).face)
val shadingN = context.lighting(face, edgeDir, (edgeAxis to Direction.AxisDirection.NEGATIVE).face)
if (!shadingP.valid && !shadingN.valid) {
return vertex.shade(context.brightness(offset) brMul fallbackDimming, context.color(offset) colorMul fallbackDimming)
}
if (!shadingP.valid) return vertex.shade(shadingN)
if (!shadingN.valid) return vertex.shade(shadingP)
vertex.shade(shadingP, shadingN, weightP, weightN)
}
override fun rotate(rot: Rotation) = EdgeInterpolateFallback(face.rotate(rot), edgeDir.rotate(rot), pos)
}
class CornerInterpolateDimming(val face1: Direction, val face2: Direction, val edgeDir: Direction,
val weight: Float, val dimming: Float, val fallbackDimming: Float = defaultCornerDimming
) : ModelLighter {
val offset = Int3(edgeDir)
override fun shade(context: LightingCtx, vertex: RenderVertex) {
var shading1 = context.lighting(face1, edgeDir, face2)
var shading2 = context.lighting(face2, edgeDir, face1)
var weight1 = weight
var weight2 = 1.0f - weight
if (!shading1.valid && !shading2.valid) {
return vertex.shade(context.brightness(offset) brMul fallbackDimming, context.color(offset) colorMul fallbackDimming)
}
if (!shading1.valid) { shading1 = shading2; weight1 *= dimming }
if (!shading2.valid) { shading2 = shading1; weight2 *= dimming }
vertex.shade(shading1, shading2, weight1, weight2)
}
override fun rotate(rot: Rotation) =
CornerInterpolateDimming(face1.rotate(rot), face2.rotate(rot), edgeDir.rotate(rot), weight, dimming, fallbackDimming)
}
class FaceCenter(val face: Direction): ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) {
vertex.red = 0.0f; vertex.green = 0.0f; vertex.blue = 0.0f;
val b = IntArray(4)
boxFaces[face].allCorners.forEachIndexed { idx, corner ->
val shading = context.lighting(face, corner.first, corner.second)
vertex.red += shading.red
vertex.green += shading.green
vertex.blue += shading.blue
b[idx] = shading.brightness
}
vertex.apply { red *= 0.25f; green *= 0.25f; blue *= 0.25f }
vertex.brightness = brSum(0.25f, *b)
}
override fun rotate(rot: Rotation) = FaceCenter(face.rotate(rot))
}
class FaceFlat(val face: Direction): ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) {
vertex.shade(context.brightness(face.offset), context.color(Int3.zero))
}
override fun rotate(rot: Rotation): ModelLighter = FaceFlat(face.rotate(rot))
}
class FlatOffset(val offset: Int3): ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) {
vertex.brightness = context.brightness(offset)
vertex.setColor(context.color(offset))
}
override fun rotate(rot: Rotation): ModelLighter = this
}
class FlatOffsetNoColor(val offset: Int3): ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) {
vertex.brightness = context.brightness(offset)
vertex.red = 1.0f; vertex.green = 1.0f; vertex.blue = 1.0f
}
override fun rotate(rot: Rotation): ModelLighter = this
}

View File

@@ -1,3 +0,0 @@
@file:JvmName("PixelFormat")
package mods.betterfoliage.render.lighting

View File

@@ -0,0 +1,147 @@
package mods.betterfoliage.render.lighting
import mods.betterfoliage.chunk.BlockCtx
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.BlockModelRenderer
import net.minecraft.util.Direction
import net.minecraft.util.math.BlockPos
import net.minecraft.world.ILightReader
data class LightingData(
@JvmField var packedLight: Int = 0,
@JvmField var colorMultiplier: Float = 1.0f
) {
fun mixFrom(corner: LightingData, side1: LightingData, side2: LightingData, center: LightingData) {
colorMultiplier =
(center.colorMultiplier + side1.colorMultiplier + side2.colorMultiplier + corner.colorMultiplier) * 0.25f
packedLight = (
center.packedLight +
(side1.packedLight.takeUnless { it == 0 } ?: center.packedLight) +
(side2.packedLight.takeUnless { it == 0 } ?: center.packedLight) +
(corner.packedLight.takeUnless { it == 0 } ?: center.packedLight)
).let { sum -> (sum shr 2) and 0xFF00FF }
}
}
/**
* Replacement for [BlockModelRenderer.AmbientOcclusionFace]
* This gets called on a LOT, so object instantiation is avoided.
* Not thread-safe, always use a [ThreadLocal] instance
*/
class VanillaAoCalculator {
lateinit var world: ILightReader
/** [blockPos] is used to get block-related information (i.e. tint, opacity, etc.)
* [lightPos] is used to get light-related information
* this facilitates masquerade rendering of blocks */
lateinit var blockPos: BlockPos
lateinit var lightPos: BlockPos
private val probe = LightProbe(BlockModelRenderer.CACHE_COMBINED_LIGHT.get())
val isValid = BooleanArray(6)
val aoData = Array(24) { LightingData() }
// scratchpad values used during calculation
private val centerAo = LightingData()
private val sideAo = Array(4) { LightingData() }
private val cornerAo = Array(4) { LightingData() }
private val isOccluded = BooleanArray(4)
fun reset(ctx: BlockCtx) {
world = ctx.world; blockPos = ctx.pos; lightPos = ctx.pos
(0 until 6).forEach { isValid[it] = false }
}
fun fillLightData(lightFace: Direction, isOpaque: Boolean? = null) {
if (!isValid[lightFace.ordinal]) calculate(lightFace, isOpaque)
}
/**
* Replicate [BlockModelRenderer.AmbientOcclusionFace.updateVertexBrightness]
* Does not handle interpolation for non-cubic models, that should be
* done in a [VanillaVertexLighter]
* @param lightFace face of the block to calculate
* @param forceFull force full-block status for lighting calculation, null for auto
*/
private fun calculate(lightFace: Direction, forceFull: Boolean?) {
if (isValid[lightFace.ordinal]) return
val sideHelper = AoSideHelper.forSide[lightFace.ordinal]
// Bit 0 of the bitset in vanilla calculations
// true if the block model is planar with the block boundary
val isFullBlock = forceFull ?: world.getBlockState(blockPos).isCollisionShapeOpaque(world, blockPos)
val lightOrigin = if (isFullBlock) lightPos.offset(lightFace) else lightPos
// AO calculation for the face center
probe.position { setPos(lightOrigin) }.writeTo(centerAo)
if (!isFullBlock && !probe.position { move(lightFace) }.state.isOpaqueCube(world, probe.pos)) {
// if the neighboring block in the lightface direction is
// transparent (non-opaque), use its packed light instead of our own
// (if our block is a full block, we are already using this value)
centerAo.packedLight = probe.packedLight
}
// AO calculation for the 4 sides
sideHelper.sides.forEachIndexed { sideIdx, sideDir ->
// record light data in the block 1 step to the side
probe.position { setPos(lightOrigin).move(sideDir) }.writeTo(sideAo[sideIdx])
// side is considered occluded if the block 1 step to that side and
// 1 step forward (in the lightface direction) is not fully transparent
isOccluded[sideIdx] = probe.position { move(lightFace) }.isNonTransparent
}
// AO Calculation for the 4 corners
AoSideHelper.faceCornersIdx.forEachIndexed { cornerIdx, sideIndices ->
val bothOccluded = isOccluded[sideIndices.first] && isOccluded[sideIndices.second]
if (bothOccluded) cornerAo[cornerIdx].apply {
// if both sides are occluded, just use the packed light for one of the sides instead
val copyFrom = sideAo[sideIndices.first]
packedLight = copyFrom.packedLight; colorMultiplier = copyFrom.colorMultiplier
}
else {
// lookup actual packed light from the cornering block in the world
probe.position {
setPos(lightOrigin)
.move(sideHelper.sides[sideIndices.first])
.move(sideHelper.sides[sideIndices.second])
}.writeTo(cornerAo[cornerIdx])
}
}
// Calculate and store final interpolated value for each corner
AoSideHelper.faceCornersIdx.forEachIndexed { cornerIdx, sideIndices ->
val aoIdx = sideHelper.aoIndex[cornerIdx]
aoData[aoIdx].mixFrom(
cornerAo[cornerIdx],
sideAo[sideIndices.first],
sideAo[sideIndices.second],
centerAo
)
}
isValid[lightFace.ordinal] = true
}
inner class LightProbe(
val cache: BlockModelRenderer.Cache
) {
lateinit var state: BlockState
val pos = BlockPos.Mutable()
val packedLight: Int get() = cache.getPackedLight(state, world, pos)
val colorMultiplier: Float get() = cache.getBrightness(state, world, pos)
val isNonTransparent: Boolean get() = state.getOpacity(world, pos) > 0
fun writeTo(data: LightingData) {
data.packedLight = packedLight
data.colorMultiplier = colorMultiplier
}
inline fun position(func: BlockPos.Mutable.() -> Unit): LightProbe {
pos.func()
state = world.getBlockState(pos)
return this
}
}
}

View File

@@ -0,0 +1,133 @@
package mods.betterfoliage.render.lighting
import mods.betterfoliage.render.old.HalfBakedQuad
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.EPSILON
import mods.betterfoliage.util.minBy
import net.minecraft.client.renderer.color.BlockColors
import net.minecraft.util.Direction
import net.minecraft.util.Direction.*
import net.minecraft.util.Direction.Axis
import net.minecraftforge.client.model.pipeline.LightUtil
import kotlin.math.abs
class VanillaQuadLighting {
val packedLight = IntArray(4)
val colorMultiplier = FloatArray(4)
val tint = FloatArray(3)
val calc = VanillaAoCalculator()
lateinit var blockColors: BlockColors
fun updateBlockTint(tintIndex: Int) {
if (tintIndex == -1) {
tint[0] = 1.0f; tint[1] = 1.0f; tint[2] = 1.0f
} else {
val state = calc.world.getBlockState(calc.blockPos)
blockColors.getColor(state, calc.world, calc.blockPos, tintIndex).let { blockTint ->
tint[0] = (blockTint shr 16 and 255).toFloat() / 255.0f
tint[1] = (blockTint shr 8 and 255).toFloat() / 255.0f
tint[2] = (blockTint and 255).toFloat() / 255.0f
}
}
}
fun applyDiffuseLighting(face: Direction) {
val factor = LightUtil.diffuseLight(face)
tint[0] *= factor; tint[1] *= factor; tint[2] *= factor
}
}
abstract class VanillaVertexLighter {
abstract fun updateLightmapAndColor(quad: HalfBakedQuad, lighting: VanillaQuadLighting)
inline fun VanillaQuadLighting.updateWithCornerAo(quad: HalfBakedQuad, func: (Double3)->Int?) {
quad.raw.verts.forEachIndexed { idx, vertex ->
func(vertex.xyz)?.let {
packedLight[idx] = calc.aoData[it].packedLight
colorMultiplier[idx] = calc.aoData[it].colorMultiplier
}
}
}
}
object VanillaFullBlockLighting : VanillaVertexLighter() {
override fun updateLightmapAndColor(quad: HalfBakedQuad, lighting: VanillaQuadLighting) {
// TODO bounds checking
val face = quad.raw.face()
lighting.calc.fillLightData(face, true)
lighting.updateWithCornerAo(quad) { nearestCornerOnFace(it, face) }
lighting.updateBlockTint(quad.baked.tintIndex)
if (quad.baked.shouldApplyDiffuseLighting()) lighting.applyDiffuseLighting(face)
}
}
object RoundLeafLighting : VanillaVertexLighter() {
override fun updateLightmapAndColor(quad: HalfBakedQuad, lighting: VanillaQuadLighting) {
val angles = getAngles45(quad)?.let { normalFaces ->
lighting.calc.fillLightData(normalFaces.first)
lighting.calc.fillLightData(normalFaces.second)
if (normalFaces.first != UP && normalFaces.second != UP) lighting.calc.fillLightData(UP)
lighting.updateWithCornerAo(quad) { vertex ->
val isUp = vertex.y > 0.5f
val cornerUndir = AoSideHelper.getCornerUndir(vertex.x, vertex.y, vertex.z)
val preferredFace = if (isUp) UP else normalFaces.minBy { faceDistance(it, vertex) }
AoSideHelper.boxCornersDirFromUndir[preferredFace.ordinal][cornerUndir]
}
lighting.updateBlockTint(quad.baked.tintIndex)
}
}
}
class LightingPreferredFace(val face: Direction) : VanillaVertexLighter() {
override fun updateLightmapAndColor(quad: HalfBakedQuad, lighting: VanillaQuadLighting) {
lighting.calc.fillLightData(face)
lighting.updateWithCornerAo(quad) { nearestCornerOnFace(it, face) }
lighting.updateBlockTint(quad.baked.tintIndex)
}
}
/**
* Return the directed box corner index for the corner nearest the given vertex,
* which is on the given face. May return null if the vertex is closest to
* one of the opposite 4 corners
*/
fun nearestCornerOnFace(pos: Double3, face: Direction): Int? {
val cornerUndir = AoSideHelper.getCornerUndir(pos.x, pos.y, pos.z)
return AoSideHelper.boxCornersDirFromUndir[face.ordinal][cornerUndir]
}
/**
* If the quad normal approximately bisects 2 axes at a 45 degree angle,
* and is approximately perpendicular to the third, returns the 2 directions
* the quad normal points towards.
* Returns null otherwise.
*/
fun getAngles45(quad: HalfBakedQuad): Pair<Direction, Direction>? {
val normal = quad.raw.normal
// one of the components must be close to zero
val zeroAxis = when {
abs(normal.x) < EPSILON -> Axis.X
abs(normal.y) < EPSILON -> Axis.Y
abs(normal.z) < EPSILON -> Axis.Z
else -> return null
}
// the other two must be of similar magnitude
val diff = when(zeroAxis) {
Axis.X -> abs(abs(normal.y) - abs(normal.z))
Axis.Y -> abs(abs(normal.x) - abs(normal.z))
Axis.Z -> abs(abs(normal.x) - abs(normal.y))
}
if (diff > EPSILON) return null
return when(zeroAxis) {
Axis.X -> Pair(if (normal.y > 0.0f) UP else DOWN, if (normal.z > 0.0f) SOUTH else NORTH)
Axis.Y -> Pair(if (normal.x > 0.0f) EAST else WEST, if (normal.z > 0.0f) SOUTH else NORTH)
Axis.Z -> Pair(if (normal.x > 0.0f) EAST else WEST, if (normal.y > 0.0f) UP else DOWN)
}
}
fun faceDistance(face: Direction, pos: Double3) = when(face) {
WEST -> pos.x; EAST -> 1.0 - pos.x
DOWN -> pos.y; UP -> 1.0 - pos.y
NORTH -> pos.z; SOUTH -> 1.0 - pos.z
}

View File

@@ -1,146 +0,0 @@
package mods.betterfoliage.render.lighting
import mods.betterfoliage.render.old.CombinedContext
import mods.betterfoliage.render.old.Quad
import mods.betterfoliage.render.old.Vertex
import mods.betterfoliage.util.Double3
import mods.betterfoliage.util.Rotation
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.Direction.*
import java.awt.Color
typealias QuadIconResolver = (CombinedContext, Int, Quad) -> TextureAtlasSprite?
typealias PostProcessLambda = RenderVertex.(CombinedContext, Int, Quad, Int, Vertex) -> Unit
@Suppress("NOTHING_TO_INLINE")
class RenderVertex {
var x: Double = 0.0
var y: Double = 0.0
var z: Double = 0.0
var u: Double = 0.0
var v: Double = 0.0
var brightness: Int = 0
var red: Float = 0.0f
var green: Float = 0.0f
var blue: Float = 0.0f
val rawData = IntArray(7)
fun init(vertex: Vertex, rot: Rotation, trans: Double3): RenderVertex {
val result = vertex.xyz.rotate(rot) + trans
x = result.x; y = result.y; z = result.z
return this
}
fun init(vertex: Vertex): RenderVertex {
x = vertex.xyz.x; y = vertex.xyz.y; z = vertex.xyz.z;
u = vertex.uv.u; v = vertex.uv.v
return this
}
fun translate(trans: Double3): RenderVertex { x += trans.x; y += trans.y; z += trans.z; return this }
fun rotate(rot: Rotation): RenderVertex {
if (rot === Rotation.identity) return this
val rotX = rot.rotatedComponent(EAST, x, y, z)
val rotY = rot.rotatedComponent(UP, x, y, z)
val rotZ = rot.rotatedComponent(SOUTH, x, y, z)
x = rotX; y = rotY; z = rotZ
return this
}
inline fun rotateUV(n: Int): RenderVertex {
when (n % 4) {
1 -> { val t = v; v = -u; u = t; return this }
2 -> { u = -u; v = -v; return this }
3 -> { val t = -v; v = u; u = t; return this }
else -> { return this }
}
}
inline fun mirrorUV(mirrorU: Boolean, mirrorV: Boolean) {
if (mirrorU) u = -u
if (mirrorV) v = -v
}
inline fun setIcon(icon: TextureAtlasSprite): RenderVertex {
u = (icon.maxU - icon.minU) * (u + 0.5) + icon.minU
v = (icon.maxV - icon.minV) * (v + 0.5) + icon.minV
return this
}
inline fun setGrey(level: Float) {
val grey = Math.min((red + green + blue) * 0.333f * level, 1.0f)
red = grey; green = grey; blue = grey
}
inline fun multiplyColor(color: Int) {
red *= (color shr 16 and 255) / 256.0f
green *= (color shr 8 and 255) / 256.0f
blue *= (color and 255) / 256.0f
}
inline fun setColor(color: Int) {
red = (color shr 16 and 255) / 256.0f
green = (color shr 8 and 255) / 256.0f
blue = (color and 255) / 256.0f
}
}
/** List of bit-shift offsets in packed brightness values where meaningful (4-bit) data is contained. */
var brightnessComponents = listOf(20, 4)
/** Multiply the components of this packed brightness value with the given [Float]. */
infix fun Int.brMul(f: Float): Int {
val weight = (f * 256.0f).toInt()
var result = 0
brightnessComponents.forEach { shift ->
val raw = (this shr shift) and 15
val weighted = (raw) * weight / 256
result = result or (weighted shl shift)
}
return result
}
/** Multiply the components of this packed color value with the given [Float]. */
infix fun Int.colorMul(f: Float): Int {
val weight = (f * 256.0f).toInt()
val red = (this shr 16 and 255) * weight / 256
val green = (this shr 8 and 255) * weight / 256
val blue = (this and 255) * weight / 256
return (red shl 16) or (green shl 8) or blue
}
/** Sum the components of all packed brightness values given. */
fun brSum(multiplier: Float?, vararg brightness: Int): Int {
val sum = Array(brightnessComponents.size) { 0 }
brightnessComponents.forEachIndexed { idx, shift -> brightness.forEach { br ->
val comp = (br shr shift) and 15
sum[idx] += comp
} }
var result = 0
brightnessComponents.forEachIndexed { idx, shift ->
val comp = if (multiplier == null)
((sum[idx]) shl shift)
else
((sum[idx].toFloat() * multiplier).toInt() shl shift)
result = result or comp
}
return result
}
fun brWeighted(br1: Int, weight1: Float, br2: Int, weight2: Float): Int {
val w1int = (weight1 * 256.0f + 0.5f).toInt()
val w2int = (weight2 * 256.0f + 0.5f).toInt()
var result = 0
brightnessComponents.forEachIndexed { idx, shift ->
val comp1 = (br1 shr shift) and 15
val comp2 = (br2 shr shift) and 15
val compWeighted = (comp1 * w1int + comp2 * w2int) / 256
result = result or ((compWeighted and 15) shl shift)
}
return result
}
data class HSB(var hue: Float, var saturation: Float, var brightness: Float) {
companion object {
fun fromColor(color: Int): HSB {
val hsbVals = Color.RGBtoHSB((color shr 16) and 255, (color shr 8) and 255, color and 255, null)
return HSB(hsbVals[0], hsbVals[1], hsbVals[2])
}
}
val asColor: Int get() = Color.HSBtoRGB(hue, saturation, brightness)
}