Skip to content

Commit

Permalink
Render block item entities and fix item entity bobbing to match vanil…
Browse files Browse the repository at this point in the history
…la better
  • Loading branch information
stackotter committed Jun 8, 2024
1 parent fa8b95f commit be4a1ea
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 50 deletions.
3 changes: 2 additions & 1 deletion Sources/Core/Renderer/ChunkUniforms.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import DeltaCore
import FirebladeMath

public struct ChunkUniforms {
Expand All @@ -10,6 +11,6 @@ public struct ChunkUniforms {
}

public init() {
transformation = Mat4x4f(diagonal: 1)
transformation = MatrixUtil.identity
}
}
67 changes: 56 additions & 11 deletions Sources/Core/Renderer/Entity/EntityRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public struct EntityRenderer: Renderer {

/// The render pipeline state for rendering entities. Does not have blending enabled.
private var renderPipelineState: MTLRenderPipelineState
/// The render pipeline state for rendering block entities and block item entities.
private var blockRenderPipelineState: MTLRenderPipelineState
/// The buffer containing the uniforms for all rendered entities.
private var instanceUniformsBuffer: MTLBuffer?
/// The buffer containing the hit box vertices. They form a basic cube and instanced rendering is used to render the cube once for each entity.
Expand All @@ -21,6 +23,7 @@ public struct EntityRenderer: Renderer {
private var indexCount: Int

private var entityTexturePalette: MetalTexturePalette
private var blockTexturePalette: MetalTexturePalette

private var entityModelPalette: EntityModelPalette
private var itemModelPalette: ItemModelPalette
Expand All @@ -40,27 +43,43 @@ public struct EntityRenderer: Renderer {
client: Client,
device: MTLDevice,
commandQueue: MTLCommandQueue,
profiler: Profiler<RenderingMeasurement>
profiler: Profiler<RenderingMeasurement>,
blockTexturePalette: MetalTexturePalette
) throws {
self.client = client
self.device = device
self.commandQueue = commandQueue
self.profiler = profiler
self.blockTexturePalette = blockTexturePalette

// Load library
// TODO: Avoid loading library again and again
let library = try MetalUtil.loadDefaultLibrary(device)
let vertexFunction = try MetalUtil.loadFunction("entityVertexShader", from: library)
let fragmentFunction = try MetalUtil.loadFunction("entityFragmentShader", from: library)
let blockVertexFunction = try MetalUtil.loadFunction("chunkVertexShader", from: library)
let blockFragmentFunction = try MetalUtil.loadFunction("chunkFragmentShader", from: library)

// Create render pipeline state
renderPipelineState = try MetalUtil.makeRenderPipelineState(
device: device,
label: "dev.stackotter.delta-client.EntityRenderer",
label: "EntityRenderer.renderPipelineState",
vertexFunction: vertexFunction,
fragmentFunction: fragmentFunction,
blendingEnabled: false
)

// TODO: Consider supporting OIT here too? Probably not of much use cause most block item
// entities aren't translucent, and there should never be many instances of them since
// item entities merge.
blockRenderPipelineState = try MetalUtil.makeRenderPipelineState(
device: device,
label: "EntityRenderer.blockRenderPipelineState",
vertexFunction: blockVertexFunction,
fragmentFunction: blockFragmentFunction,
blendingEnabled: true
)

// Create hitbox geometry (hitboxes are rendered using instancing)
var geometry = Self.createHitBoxGeometry(color: Self.hitBoxColor)
indexCount = geometry.indices.count
Expand Down Expand Up @@ -107,6 +126,8 @@ public struct EntityRenderer: Renderer {

// Get all renderable entities
var geometry = Geometry<EntityVertex>()
var blockGeometry = Geometry<BlockVertex>()
var translucentBlockGeometry = SortableMesh(uniforms: ChunkUniforms())
client.game.accessNexus { nexus in
// If the player is in first person view we don't render them
profiler.push(.getEntities)
Expand Down Expand Up @@ -159,24 +180,48 @@ public struct EntityRenderer: Renderer {
entityModelPalette: entityModelPalette,
itemModelPalette: itemModelPalette,
blockModelPalette: blockModelPalette,
texturePalette: entityTexturePalette,
entityTexturePalette: entityTexturePalette,
blockTexturePalette: blockTexturePalette,
hitbox: hitbox.aabb(at: position.smoothVector)
)
builder.build(into: &geometry)
builder.build(
into: &geometry,
blockGeometry: &blockGeometry,
translucentBlockGeometry: &translucentBlockGeometry
)
}
profiler.pop()
}

guard !geometry.isEmpty else {
return
if !geometry.isEmpty {
encoder.setRenderPipelineState(renderPipelineState)
encoder.setFragmentTexture(entityTexturePalette.arrayTexture, index: 0)

// TODO: Update profiler measurements
var mesh = Mesh<EntityVertex, Void>(geometry, uniforms: ())
try mesh.render(into: encoder, with: device, commandQueue: commandQueue)
}

encoder.setRenderPipelineState(renderPipelineState)
encoder.setFragmentTexture(entityTexturePalette.arrayTexture, index: 0)
if !blockGeometry.isEmpty || !translucentBlockGeometry.isEmpty {
encoder.setRenderPipelineState(blockRenderPipelineState)
encoder.setVertexBuffer(blockTexturePalette.textureStatesBuffer, offset: 0, index: 3)
encoder.setFragmentTexture(blockTexturePalette.arrayTexture, index: 0)

// TODO: Update profiler measurements
var mesh = Mesh<EntityVertex, Void>(geometry, uniforms: ())
try mesh.render(into: encoder, with: device, commandQueue: commandQueue)
if !blockGeometry.isEmpty {
var blockMesh = Mesh<BlockVertex, ChunkUniforms>(blockGeometry, uniforms: ChunkUniforms())
try blockMesh.render(into: encoder, with: device, commandQueue: commandQueue)
}

if !translucentBlockGeometry.isEmpty {
try translucentBlockGeometry.render(
viewedFrom: camera.position,
sort: true,
encoder: encoder,
device: device,
commandQueue: commandQueue
)
}
}
}

/// Creates a coloured and shaded cube to be rendered using instancing as entities' hitboxes.
Expand Down
96 changes: 73 additions & 23 deletions Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public struct EntityMeshBuilder {
public let entityModelPalette: EntityModelPalette
public let itemModelPalette: ItemModelPalette
public let blockModelPalette: BlockModelPalette
public let texturePalette: MetalTexturePalette
public let entityTexturePalette: MetalTexturePalette
public let blockTexturePalette: MetalTexturePalette
public let hitbox: AxisAlignedBoundingBox

static let colors: [Vec3f] = [
Expand All @@ -35,13 +36,30 @@ public struct EntityMeshBuilder {
[1, 1, 1],
]

func build(into geometry: inout Geometry<EntityVertex>) {
// TODO: Propagate all warnings as errors and then handle them and emit them as warnings in EntityRenderer instead
/// `blockGeometry` and `translucentBlockGeometry` are used to render block entities and block item entities.
func build(
into geometry: inout Geometry<EntityVertex>,
blockGeometry: inout Geometry<BlockVertex>,
translucentBlockGeometry: inout SortableMesh
) {
if let model = entityModelPalette.models[entityKind] {
buildModel(model, into: &geometry)
} else if let itemMetadata = entity.get(component: EntityMetadata.self)?.itemMetadata,
let itemStack = itemMetadata.slot.stack,
let itemModel = itemModelPalette.model(for: itemStack.itemId)
{
// TODO: Figure out why these bobbing constants and hardcoded translations are so weird
// (they're even still slightly off vanilla, there must be a different order of transformations
// that makes these numbers nice or something).
let time = CFAbsoluteTimeGetCurrent() * TickScheduler.defaultTicksPerSecond
let phaseOffset = Double(itemMetadata.bobbingPhaseOffset)
let verticalOffset = Float(Foundation.sin(time / 10 + phaseOffset)) / 8 * 3
let spinAngle = -Float((time / 20 + phaseOffset).remainder(dividingBy: 2 * .pi))
let bob =
MatrixUtil.translationMatrix(Vec3f(0, verticalOffset, 0))
* MatrixUtil.rotationMatrix(y: spinAngle)

switch itemModel {
case let .entity(identifier, transforms):
// Remove identifier prefix (entity model palette doesn't have any `item/` or `entity/` prefixes).
Expand All @@ -53,26 +71,51 @@ public struct EntityMeshBuilder {
return
}

var transformation = transforms.ground
let time = CFAbsoluteTimeGetCurrent() * TickScheduler.defaultTicksPerSecond
let phaseOffset = Double(itemMetadata.bobbingPhaseOffset)
let bob = Float(Foundation.sin(time / 10 + phaseOffset))
let scaleY = (transformation * Vec4f(0, 1, 0, 0)).magnitude
let verticalOffset = bob + 0.25 * scaleY
transformation *= MatrixUtil.translationMatrix(Vec3f(0, verticalOffset, 0))
transformation *= MatrixUtil.rotationMatrix(
y: -Float((time / 20 + phaseOffset).remainder(dividingBy: 2 * .pi))
)

let transformation =
bob * transforms.ground * MatrixUtil.translationMatrix(Vec3f(0, 11.0 / 64, 0))
buildModel(
entityModel,
textureIdentifier: entityIdentifier,
transformation: transformation,
into: &geometry
)
case .blockModel, .layered:
case let .blockModel(id):
guard let blockModel = blockModelPalette.model(for: id, at: nil) else {
log.warning(
"Missing block model for item entity (block id: \(id), item id: \(itemStack.itemId))"
)
return
}

// TODO: Don't just use dummy lighting
var neighbourLightLevels: [Direction: LightLevel] = [:]
for direction in Direction.allDirections {
neighbourLightLevels[direction] = LightLevel(sky: 15, block: 0)
}

let transformation =
MatrixUtil.translationMatrix(Vec3f(-0.5, 0, -0.5))
* bob
* MatrixUtil.scalingMatrix(0.25)
* MatrixUtil.translationMatrix(Vec3f(0, 7.0 / 32.0, 0))
* MatrixUtil.rotationMatrix(y: yaw + .pi)
* MatrixUtil.translationMatrix(position)
let builder = BlockMeshBuilder(
model: blockModel,
position: .zero,
modelToWorld: transformation,
culledFaces: [],
lightLevel: LightLevel(sky: 15, block: 0),
neighbourLightLevels: [:],
tintColor: Vec3f(1, 1, 1),
blockTexturePalette: blockTexturePalette.palette
)
var translucentElement = SortableMeshElement()
builder.build(into: &blockGeometry, translucentGeometry: &translucentElement)
translucentBlockGeometry.add(translucentElement)
case .layered:
buildAABB(hitbox, into: &geometry)
case .empty, .blockModel, .layered:
case .empty:
break
}
} else {
Expand Down Expand Up @@ -117,6 +160,7 @@ public struct EntityMeshBuilder {
}
}

/// The unit of `transformation` is blocks.
func buildModel(
_ model: JSONEntityModel,
textureIdentifier: Identifier? = nil,
Expand All @@ -126,7 +170,7 @@ public struct EntityMeshBuilder {
let baseTextureIdentifier = textureIdentifier ?? entityKind
let texture: Int?
if let identifier = Self.hardcodedTextureIdentifiers[baseTextureIdentifier] {
texture = texturePalette.textureIndex(for: identifier)
texture = entityTexturePalette.textureIndex(for: identifier)
} else {
// Entity textures can be in all sorts of structures so we just have a few
// educated guesses for now.
Expand All @@ -139,8 +183,8 @@ public struct EntityMeshBuilder {
name: "entity/\(baseTextureIdentifier.name)/\(baseTextureIdentifier.name)"
)
texture =
texturePalette.textureIndex(for: textureIdentifier)
?? texturePalette.textureIndex(for: nestedTextureIdentifier)
entityTexturePalette.textureIndex(for: textureIdentifier)
?? entityTexturePalette.textureIndex(for: nestedTextureIdentifier)
}

for (index, submodel) in model.models.enumerated() {
Expand All @@ -154,6 +198,7 @@ public struct EntityMeshBuilder {
}
}

/// The unit of `transformation` is blocks.
func buildSubmodel(
_ submodel: JSONEntityModel.Submodel,
index: Int,
Expand All @@ -166,7 +211,7 @@ public struct EntityMeshBuilder {
let translation = submodel.translate ?? .zero
transformation =
MatrixUtil.rotationMatrix(-MathUtil.radians(from: rotation))
* MatrixUtil.translationMatrix(translation)
* MatrixUtil.translationMatrix(translation / 16)
* transformation
}

Expand Down Expand Up @@ -197,6 +242,7 @@ public struct EntityMeshBuilder {
}
}

/// The unit of `transformation` is 16 units per block.
func buildBox(
_ box: JSONEntityModel.Box,
color: Vec3f,
Expand Down Expand Up @@ -278,22 +324,26 @@ public struct EntityMeshBuilder {
uvSize.y *= -1
}

let textureSize = Vec2f(
Float(entityTexturePalette.palette.width),
Float(entityTexturePalette.palette.height)
)
let uvs = [
uvOrigin,
uvOrigin + Vec2f(0, uvSize.y),
uvOrigin + Vec2f(uvSize.x, uvSize.y),
uvOrigin + Vec2f(uvSize.x, 0),
].map {
$0 / Vec2f(Float(texturePalette.palette.width), Float(texturePalette.palette.height))
].map { pixelUV in
pixelUV / textureSize
}

let faceVertexPositions = CubeGeometry.faceVertices[direction.rawValue]
for (uv, vertexPosition) in zip(uvs, faceVertexPositions) {
var position = vertexPosition * boxSize + boxPosition
position /= 16
position =
(Vec4f(position, 1) * transformation * MatrixUtil.rotationMatrix(yaw + .pi, around: .y))
(Vec4f(position, 1) * transformation * MatrixUtil.rotationMatrix(y: yaw + .pi))
.xyz
position /= 16
position += self.position
let vertex = EntityVertex(
x: position.x,
Expand Down
11 changes: 7 additions & 4 deletions Sources/Core/Renderer/World/WorldRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ public final class WorldRenderer: Renderer {
let vertexFunction = try MetalUtil.loadFunction("chunkVertexShader", from: library)
let fragmentFunction = try MetalUtil.loadFunction("chunkFragmentShader", from: library)
let transparentFragmentFunction = try MetalUtil.loadFunction(
"chunkOITFragmentShader", from: library)
"chunkOITFragmentShader",
from: library
)
let transparentCompositingVertexFunction = try MetalUtil.loadFunction(
"chunkOITCompositingVertexShader",
from: library
Expand All @@ -107,15 +109,15 @@ public final class WorldRenderer: Renderer {
// Create opaque pipeline (which also handles translucent geometry when OIT is disabled)
renderPipelineState = try MetalUtil.makeRenderPipelineState(
device: device,
label: "WorldRenderer.mainPipeline",
label: "WorldRenderer.renderPipelineState",
vertexFunction: vertexFunction,
fragmentFunction: fragmentFunction,
blendingEnabled: true
)

destroyOverlayRenderPipelineState = try MetalUtil.makeRenderPipelineState(
device: device,
label: "WorldRenderer.destroyOverlayPipeline",
label: "WorldRenderer.destroyOverlayRenderPipelineState",
vertexFunction: vertexFunction,
fragmentFunction: fragmentFunction,
blendingEnabled: true,
Expand Down Expand Up @@ -178,7 +180,8 @@ public final class WorldRenderer: Renderer {
client: client,
device: device,
commandQueue: commandQueue,
profiler: profiler
profiler: profiler,
blockTexturePalette: texturePalette
)

// Create world mesh
Expand Down
Loading

0 comments on commit be4a1ea

Please sign in to comment.