Merge branch 'quad-format-rewrite'

This commit is contained in:
mcrcortex
2024-01-30 14:37:20 +10:00
44 changed files with 2148 additions and 877 deletions

View File

@@ -46,7 +46,9 @@ dependencies {
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
modImplementation "maven.modrinth:sodium:mc1.20.3-0.5.5"
//TODO: this is to eventually not need sodium installed as atm its just used for parsing shaders
modRuntimeOnly "maven.modrinth:sodium:mc1.20.3-0.5.5"
modCompileOnly "maven.modrinth:sodium:mc1.20.3-0.5.5"
modImplementation("maven.modrinth:cloth-config:13.0.121+fabric")
modImplementation("maven.modrinth:modmenu:9.0.0")

View File

@@ -8,7 +8,7 @@ yarn_mappings=1.20.4+build.1
loader_version=0.15.0
# Mod Properties
mod_version = 0.0.2-alpha
mod_version = 0.0.3-alpha
maven_group = me.cortex
archives_base_name = zenith

View File

@@ -21,6 +21,8 @@ public class ZenithConfig {
public boolean enabled = true;
public int qualityScale = 20;
public int maxSections = 200_000;
public int geometryBufferSize = (1<<30)/8;
public int ingestThreads = 2;
public int savingThreads = 10;
public int renderThreads = 5;

View File

@@ -25,7 +25,7 @@ public class ZenithConfigScreenFactory implements ModMenuApi {
private static Screen buildConfigScreen(Screen parent, ZenithConfig config) {
ConfigBuilder builder = ConfigBuilder.create()
.setParentScreen(parent)
.setTitle(Text.translatable("title.zenith.config"));
.setTitle(Text.translatable("zenith.config.title"));
addGeneralCategory(builder, config);
@@ -61,6 +61,18 @@ public class ZenithConfigScreenFactory implements ModMenuApi {
.setDefaultValue(DEFAULT.qualityScale)
.build());
category.addEntry(entryBuilder.startIntSlider(Text.translatable("zenith.config.general.geometryBuffer"), config.geometryBufferSize, (1<<27)/8, ((1<<31)-1)/8)
.setTooltip(Text.translatable("zenith.config.general.geometryBuffer.tooltip"))
.setSaveConsumer(val -> config.geometryBufferSize = val)
.setDefaultValue(DEFAULT.geometryBufferSize)
.build());
category.addEntry(entryBuilder.startIntSlider(Text.translatable("zenith.config.general.maxSections"), config.maxSections, 100_000, 400_000)
.setTooltip(Text.translatable("zenith.config.general.maxSections.tooltip"))
.setSaveConsumer(val -> config.maxSections = val)
.setDefaultValue(DEFAULT.maxSections)
.build());
category.addEntry(entryBuilder.startIntSlider(Text.translatable("zenith.config.general.compression"), config.savingCompressionLevel, 1, 21)
.setTooltip(Text.translatable("zenith.config.general.compression.tooltip"))
.setSaveConsumer(val -> config.savingCompressionLevel = val)

View File

@@ -28,15 +28,17 @@ public class DistanceTracker {
this.minYSection = MinecraftClient.getInstance().world.getBottomSectionCoord()/2;
this.maxYSection = MinecraftClient.getInstance().world.getTopSectionCoord()/2;
this.rings[0] = new TransitionRing2D(5, MinecraftClient.getInstance().options.getViewDistance().getValue()/2, (x, z)->{
for (int y = this.minYSection; y <= this.maxYSection; y++) {
this.tracker.remLvl0(x, y, z);
}
}, (x, z) -> {
for (int y = this.minYSection; y <= this.maxYSection; y++) {
this.tracker.addLvl0(x, y, z);
}
});
if (true) {
this.rings[0] = new TransitionRing2D(5, MinecraftClient.getInstance().options.getViewDistance().getValue() / 2, (x, z) -> {
for (int y = this.minYSection; y <= this.maxYSection; y++) {
this.tracker.remLvl0(x, y, z);
}
}, (x, z) -> {
for (int y = this.minYSection; y <= this.maxYSection; y++) {
this.tracker.addLvl0(x, y, z);
}
});
}
//The rings 0+ start at 64 vanilla rd, no matter what the game is set at, that is if the game is set to 32 rd
// there will still be 32 chunks untill the first lod drop

View File

@@ -5,10 +5,6 @@ import me.cortex.zenith.client.core.rendering.*;
import me.cortex.zenith.client.core.rendering.building.RenderGenerationService;
import me.cortex.zenith.client.core.util.DebugUtil;
import me.cortex.zenith.common.world.WorldEngine;
import me.cortex.zenith.client.core.other.BiomeColour;
import me.cortex.zenith.client.core.other.BlockStateColour;
import me.cortex.zenith.client.core.other.ColourResolver;
import me.cortex.zenith.common.world.other.Mapper;
import me.cortex.zenith.client.importers.WorldImporter;
import me.cortex.zenith.common.world.storage.FragmentedStorageBackendAdaptor;
import net.minecraft.block.Block;
@@ -16,7 +12,6 @@ import net.minecraft.block.Blocks;
import net.minecraft.client.render.Camera;
import net.minecraft.client.render.Frustum;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;
import net.minecraft.world.chunk.WorldChunk;
@@ -38,18 +33,6 @@ import java.util.*;
//There is strict forward only dataflow
//Ingest -> world engine -> raw render data -> render data
public class VoxelCore {
private static final Set<Block> biomeTintableAllFaces = new HashSet<>(List.of(Blocks.OAK_LEAVES, Blocks.JUNGLE_LEAVES, Blocks.ACACIA_LEAVES, Blocks.DARK_OAK_LEAVES, Blocks.VINE, Blocks.MANGROVE_LEAVES,
Blocks.TALL_GRASS, Blocks.LARGE_FERN,
Blocks.SHORT_GRASS,
Blocks.SPRUCE_LEAVES,
Blocks.BIRCH_LEAVES,
Blocks.PINK_PETALS,
Blocks.FERN, Blocks.POTTED_FERN));
private static final Set<Block> biomeTintableUpFace = new HashSet<>(List.of(Blocks.GRASS_BLOCK));
private static final Set<Block> waterTint = new HashSet<>(List.of(Blocks.WATER));
private final WorldEngine world;
private final DistanceTracker distanceTracker;
private final RenderGenerationService renderGen;
@@ -65,13 +48,13 @@ public class VoxelCore {
//Trigger the shared index buffer loading
SharedIndexBuffer.INSTANCE.id();
this.renderer = new Gl46FarWorldRenderer();
this.renderer = new Gl46FarWorldRenderer(ZenithConfig.CONFIG.geometryBufferSize, ZenithConfig.CONFIG.maxSections);
System.out.println("Renderer initialized");
this.world = new WorldEngine(new FragmentedStorageBackendAdaptor(new File(ZenithConfig.CONFIG.storagePath)), ZenithConfig.CONFIG.ingestThreads, ZenithConfig.CONFIG.savingThreads, ZenithConfig.CONFIG.savingCompressionLevel, 5);//"storagefile.db"//"ethoslab.db"
System.out.println("World engine");
this.renderTracker = new RenderTracker(this.world, this.renderer);
this.renderGen = new RenderGenerationService(this.world,ZenithConfig.CONFIG.renderThreads, this.renderTracker::processBuildResult);
this.renderGen = new RenderGenerationService(this.world, this.renderer.getModelManager(), ZenithConfig.CONFIG.renderThreads, this.renderTracker::processBuildResult);
this.world.setDirtyCallback(this.renderTracker::sectionUpdated);
this.renderTracker.setRenderGen(this.renderGen);
System.out.println("Render tracker and generator initialized");
@@ -82,39 +65,21 @@ public class VoxelCore {
this.postProcessing = null;//new PostProcessing();
this.world.getMapper().setCallbacks(this::stateUpdate, this::biomeUpdate);
this.world.getMapper().setCallbacks(this.renderer::addBlockState, a->{});
////Resave the db incase it failed a recovery
//this.world.getMapper().forceResaveStates();
for (var state : this.world.getMapper().getStateEntries()) {
this.stateUpdate(state);
this.renderer.getModelManager().addEntry(state.id, state.state);
}
for (var biome : this.world.getMapper().getBiomeEntries()) {
this.biomeUpdate(biome);
}
System.out.println("Entry updates applied");
//this.renderer.getModelManager().updateEntry(0, Blocks.GRASS_BLOCK.getDefaultState());
System.out.println("Voxel core initialized");
}
private void stateUpdate(Mapper.StateEntry entry) {
var state = entry.state;
int tintMsk = 0;
if (biomeTintableAllFaces.contains(state.getBlock())) {
tintMsk |= (1<<6)-1;
}
if (biomeTintableUpFace.contains(state.getBlock())) {
tintMsk |= 1<<Direction.UP.getId();
}
if (waterTint.contains(state.getBlock())) {
tintMsk |= 1<<6;
}
this.renderer.enqueueUpdate(new BlockStateColour(entry.id, tintMsk, ColourResolver.resolveColour(state)));
}
private void biomeUpdate(Mapper.BiomeEntry entry) {
long dualColour = ColourResolver.resolveBiomeColour(entry.biome);
this.renderer.enqueueUpdate(new BiomeColour(entry.id, (int) dualColour, (int) (dualColour>>32)));
}
public void enqueueIngest(WorldChunk worldChunk) {
@@ -126,6 +91,7 @@ public class VoxelCore {
if (this.firstTime) {
this.distanceTracker.init(camera.getBlockPos().getX(), camera.getBlockPos().getZ());
this.firstTime = false;
//this.renderTracker.addLvl0(0,6,0);
}
this.distanceTracker.setCenter(camera.getBlockPos().getX(), camera.getBlockPos().getY(), camera.getBlockPos().getZ());
this.renderer.setupRender(frustum, camera);
@@ -136,6 +102,10 @@ public class VoxelCore {
matrices.translate(-cameraX, -cameraY, -cameraZ);
DebugUtil.setPositionMatrix(matrices);
matrices.pop();
//this.renderer.getModelManager().updateEntry(0, Blocks.DIRT_PATH.getDefaultState());
//this.renderer.getModelManager().updateEntry(0, Blocks.COMPARATOR.getDefaultState());
//this.renderer.getModelManager().updateEntry(0, Blocks.OAK_LEAVES.getDefaultState());
//int boundFB = GlStateManager.getBoundFramebuffer();
//this.postProcessing.setSize(MinecraftClient.getInstance().getFramebuffer().textureWidth, MinecraftClient.getInstance().getFramebuffer().textureHeight);
@@ -149,11 +119,15 @@ public class VoxelCore {
//TODO: have the renderer also render a bounding full face just like black boarders around lvl 0
// this is cause the terrain might not exist and so all the caves are visible causing hell for the
// occlusion culler
this.renderer.renderFarAwayOpaque(matrices, cameraX, cameraY, cameraZ);
//glBindFramebuffer(GL_FRAMEBUFFER, boundFB);
//this.postProcessing.renderPost(boundFB);
//We can render the translucent directly after as it is the furthest translucent objects
this.renderer.renderFarAwayTranslucent();
}
public void addDebugInfo(List<String> debug) {

View File

@@ -10,6 +10,11 @@ import static org.lwjgl.opengl.GL45C.glNamedBufferStorage;
public class GlBuffer extends TrackedObject {
public final int id;
private final long size;
public GlBuffer(long size) {
this(size, 0);
}
public GlBuffer(long size, int flags) {
this.id = glCreateBuffers();
this.size = size;

View File

@@ -21,10 +21,11 @@ public class GlFramebuffer extends TrackedObject {
glDeleteFramebuffers(this.id);
}
public void verify() {
public GlFramebuffer verify() {
int code;
if ((code = glCheckNamedFramebufferStatus(this.id, GL_FRAMEBUFFER)) != GL_FRAMEBUFFER_COMPLETE) {
throw new IllegalStateException("Framebuffer incomplete with error code: " + code);
}
return this;
}
}

View File

@@ -0,0 +1,22 @@
package me.cortex.zenith.client.core.model;
import java.util.Arrays;
public record ColourDepthTextureData(int[] colour, int[] depth, int width, int height) {
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
var other = ((ColourDepthTextureData)obj);
return Arrays.equals(other.colour, this.colour) && Arrays.equals(other.depth, this.depth);
}
@Override
public int hashCode() {
return (this.width * 312337173 * (Arrays.hashCode(this.colour) ^ Arrays.hashCode(this.depth))) ^ this.height;
}
@Override
public ColourDepthTextureData clone() {
return new ColourDepthTextureData(Arrays.copyOf(this.colour, this.colour.length), Arrays.copyOf(this.depth, this.depth.length), this.width, this.height);
}
}

View File

@@ -0,0 +1,523 @@
package me.cortex.zenith.client.core.model;
import com.mojang.blaze3d.platform.GlConst;
import com.mojang.blaze3d.platform.GlStateManager;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import me.cortex.zenith.client.core.gl.GlBuffer;
import me.cortex.zenith.client.core.gl.GlTexture;
import me.cortex.zenith.client.core.rendering.util.UploadStream;
import net.minecraft.block.BlockState;
import net.minecraft.block.FluidBlock;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.color.block.BlockColorProvider;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.RenderLayers;
import net.minecraft.fluid.FluidState;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.BlockRenderView;
import net.minecraft.world.LightType;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.BiomeKeys;
import net.minecraft.world.biome.ColorResolver;
import net.minecraft.world.chunk.light.LightingProvider;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.system.MemoryUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL11C.GL_NEAREST;
import static org.lwjgl.opengl.GL11C.GL_NEAREST_MIPMAP_LINEAR;
import static org.lwjgl.opengl.GL12C.GL_TEXTURE_MAX_LOD;
import static org.lwjgl.opengl.GL12C.GL_TEXTURE_MIN_LOD;
import static org.lwjgl.opengl.GL33.glDeleteSamplers;
import static org.lwjgl.opengl.GL33.glGenSamplers;
import static org.lwjgl.opengl.GL33C.glSamplerParameteri;
import static org.lwjgl.opengl.GL45C.glTextureSubImage2D;
//Manages the storage and updating of model states, textures and colours
//Also has a fast long[] based metadata lookup for when the terrain mesher needs to look up the face occlusion data
//TODO: support more than 65535 states, what should actually happen is a blockstate is registered, the model data is generated, then compared
// to all other models already loaded, if it is a duplicate, create a mapping from the id to the already loaded id, this will help with meshing aswell
// as leaves and such will be able to be merged
public class ModelManager {
public static final int MODEL_SIZE = 64;
private final ModelTextureBakery bakery;
private final GlBuffer modelBuffer;
private final GlTexture textures;
private final int blockSampler = glGenSamplers();
private final int modelTextureSize;
//Model data might also contain a constant colour if the colour resolver produces a constant colour, this saves space in the
// section buffer reverse indexing
//model data also contains if a face should be randomly rotated,flipped etc to get rid of moire effect
// this would be done in the fragment shader
//The Meta-cache contains critical information needed for meshing, colour provider bit, per-face = is empty, has alpha, is solid, full width, full height
// alpha means that some pixels have alpha values and belong in the translucent rendering layer,
// is empty means that the face is air/shouldent be rendered as there is nothing there
// is solid means that every pixel is fully opaque
// full width, height, is if the blockmodel dimentions occupy a full block, e.g. comparator, some faces do some dont and some only in a specific axis
//FIXME: the issue is e.g. leaves are translucent but the alpha value is used to colour the leaves, so a block can have alpha but still be only made up of transparent or opaque pixels
// will need to find a way to send this info to the shader via the material, if it is in the opaque phase render as transparent with blending shiz
//TODO: ADD an occlusion mask that can be queried (16x16 pixels takes up 4 longs) this mask shows what pixels are exactly occluded at the edge of the block
// so that full block occlusion can work nicely
//TODO: what might work maybe, is that all the transparent pixels should be set to the average of the other pixels
// that way the block is always "fully occluding" (if the block model doesnt cover the entire thing), maybe
// this has some issues with quad merging
//TODO: ACTUALLY, full out all the transparent pixels that are _within_ the bounding box of the model
// this will mean that when quad merging and rendering, the transparent pixels of the block where there shouldent be
// might still work???
// this has an issue with scaffolding i believe tho, so maybe make it a probability to render??? idk
private final long[] metadataCache;
//Provides a map from id -> model id as multiple ids might have the same internal model id
private final int[] idMappings;
private final Object2IntOpenHashMap<List<ColourDepthTextureData>> modelTexture2id = new Object2IntOpenHashMap<>();
public ModelManager(int modelTextureSize) {
this.modelTextureSize = modelTextureSize;
this.bakery = new ModelTextureBakery(modelTextureSize, modelTextureSize);
this.modelBuffer = new GlBuffer(MODEL_SIZE * (1<<16));
//TODO: figure out how to do mipping :blobfox_pineapple:
this.textures = new GlTexture().store(GL_RGBA8, 1, modelTextureSize*3*256,modelTextureSize*2*256);
this.metadataCache = new long[1<<16];
this.idMappings = new int[1<<20];//Max of 1 million blockstates mapping to 65k model states
Arrays.fill(this.idMappings, -1);
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MIN_LOD, 0);
glSamplerParameteri(this.blockSampler, GL_TEXTURE_MAX_LOD, 4);
this.modelTexture2id.defaultReturnValue(-1);
}
//TODO: what i need to do is seperate out fluid states from blockStates
//TODO: so need a few things, per face sizes and offsets, the sizes should be computed from the pixels and find the minimum bounding pixel
// while the depth is computed from the depth buffer data
public int addEntry(int blockId, BlockState blockState) {
if (this.idMappings[blockId] != -1) {
throw new IllegalArgumentException("Trying to add entry for duplicate id");
}
boolean isFluid = blockState.getBlock() instanceof FluidBlock;
int modelId = -1;
var textureData = this.bakery.renderFaces(blockState, 123456, isFluid);
{//Deduplicate same entries
int possibleDuplicate = this.modelTexture2id.getInt(List.of(textureData));
if (possibleDuplicate != -1) {//Duplicate found
this.idMappings[blockId] = possibleDuplicate;
modelId = possibleDuplicate;
return possibleDuplicate;
} else {//Not a duplicate so create a new entry
modelId = this.modelTexture2id.size();
this.idMappings[blockId] = modelId;
this.modelTexture2id.put(Stream.of(textureData).map(ColourDepthTextureData::clone).toList(), modelId);
}
}
var colourProvider = MinecraftClient.getInstance().getBlockColors().providers.get(Registries.BLOCK.getRawId(blockState.getBlock()));
RenderLayer blockRenderLayer = null;
if (blockState.getBlock() instanceof FluidBlock) {
blockRenderLayer = RenderLayers.getFluidLayer(blockState.getFluidState());
} else {
blockRenderLayer = RenderLayers.getBlockLayer(blockState);
}
int checkMode = blockRenderLayer==RenderLayer.getSolid()?TextureUtils.WRITE_CHECK_STENCIL:TextureUtils.WRITE_CHECK_ALPHA;
long uploadPtr = UploadStream.INSTANCE.upload(this.modelBuffer, (long) modelId * MODEL_SIZE, MODEL_SIZE);
//TODO: implement;
// TODO: if it has a constant colour instead... idk why (apparently for things like spruce leaves)?? but premultiply the texture data by the constant colour
boolean hasBiomeColourResolver = false;
if (colourProvider != null) {
hasBiomeColourResolver = isBiomeDependentColour(colourProvider, blockState);
}
//TODO: special case stuff like vines and glow lichen, where it can be represented by a single double sided quad
// since that would help alot with perf of lots of vines, can be done by having one of the faces just not exist and the other be in no occlusion mode
var sizes = this.computeModelDepth(textureData, checkMode);
//TODO: THIS, note this can be tested for in 2 ways, re render the model with quad culling disabled and see if the result
// is the same, (if yes then needs double sided quads)
// another way to test it is if e.g. up and down havent got anything rendered but the sides do (e.g. all plants etc)
boolean needsDoubleSidedQuads = (sizes[0] < -0.1 && sizes[1] < -0.1) || (sizes[2] < -0.1 && sizes[3] < -0.1) || (sizes[4] < -0.1 && sizes[5] < -0.1);
//Each face gets 1 byte, with the top 2 bytes being for whatever
long metadata = 0;
metadata |= hasBiomeColourResolver?1:0;
metadata |= blockRenderLayer == RenderLayer.getTranslucent()?2:0;
metadata |= needsDoubleSidedQuads?4:0;
metadata |= (!blockState.getFluidState().isEmpty())?8:0;//Has a fluid state accosiacted with it
//TODO: add a bunch of control config options for overriding/setting options of metadata for each face of each type
for (int face = 5; face != -1; face--) {//In reverse order to make indexing into the metadata long easier
long faceUploadPtr = uploadPtr + 4L * face;//Each face gets 4 bytes worth of data
metadata <<= 8;
float offset = sizes[face];
if (offset < -0.1) {//Face is empty, so ignore
metadata |= 0xFF;//Mark the face as non-existent
//Set to -1 as safepoint
MemoryUtil.memPutInt(faceUploadPtr, -1);
continue;
}
var faceSize = TextureUtils.computeBounds(textureData[face], checkMode);
int writeCount = TextureUtils.getWrittenPixelCount(textureData[face], checkMode);
boolean faceCoversFullBlock = faceSize[0] == 0 && faceSize[2] == 0 &&
faceSize[1] == (this.modelTextureSize-1) && faceSize[3] == (this.modelTextureSize-1);
metadata |= faceCoversFullBlock?2:0;
//TODO: add alot of config options for the following
boolean occludesFace = true;
occludesFace &= blockRenderLayer != RenderLayer.getTranslucent();//If its translucent, it doesnt occlude
//TODO: make this an option, basicly if the face is really close, it occludes otherwise it doesnt
occludesFace &= offset < 0.1;//If the face is rendered far away from the other face, then it doesnt occlude
if (occludesFace) {
occludesFace &= ((float)writeCount)/(this.modelTextureSize * this.modelTextureSize) > 0.9;// only occlude if the face covers more than 90% of the face
}
metadata |= occludesFace?1:0;
boolean canBeOccluded = true;
//TODO: make this an option on how far/close
canBeOccluded &= offset < 0.3;//If the face is rendered far away from the other face, then it cant be occluded
metadata |= canBeOccluded?4:0;
//Scale face size from 0->this.modelTextureSize-1 to 0->15
for (int i = 0; i < 4; i++) {
faceSize[i] = Math.round((((float)faceSize[i])/(this.modelTextureSize-1))*15);
}
int faceModelData = 0;
faceModelData |= faceSize[0] | (faceSize[1]<<4) | (faceSize[2]<<8) | (faceSize[3]<<12);
faceModelData |= Math.round(offset*63)<<16;//Change the scale from 0->1 (ends inclusive) float to 0->63 (6 bits) NOTE! that 63 == 1.0f meaning its shifted all the way to the other side of the model
//Still have 11 bits free
//Stuff like fences are solid, however they have extra side piece that mean it needs to have discard on
int area = (faceSize[1]-faceSize[0]+1) * (faceSize[3]-faceSize[2]+1);
boolean needsAlphaDiscard = ((float)writeCount)/area<0.9;//If the amount of area covered by written pixels is less than a threashold, disable discard as its not needed
needsAlphaDiscard |= blockRenderLayer != RenderLayer.getSolid();
faceModelData |= needsAlphaDiscard?1<<22:0;
faceModelData |= (!faceCoversFullBlock)?1<<23:0;
MemoryUtil.memPutInt(faceUploadPtr, faceModelData);
}
this.metadataCache[modelId] = metadata;
uploadPtr += 4*6;
//Have 40 bytes free for remaining model data
// todo: put in like the render layer type ig? along with colour resolver info
int modelFlags = 0;
modelFlags |= colourProvider != null?1:0;
modelFlags |= hasBiomeColourResolver?2:0;//Basicly whether to use the next int as a colour or as a base index/id into a colour buffer for biome dependent colours
//modelFlags |= blockRenderLayer == RenderLayer.getSolid()?0:1;// should discard alpha
MemoryUtil.memPutInt(uploadPtr, modelFlags);
//Temporary override to always be non biome specific
if (colourProvider == null) {
MemoryUtil.memPutInt(uploadPtr + 4, -1);//Set the default to nothing so that its faster on the gpu
} else if ((!hasBiomeColourResolver) || true) {
Biome defaultBiome = MinecraftClient.getInstance().world.getRegistryManager().get(RegistryKeys.BIOME).get(BiomeKeys.PLAINS);
MemoryUtil.memPutInt(uploadPtr + 4, captureColourConstant(colourProvider, blockState, defaultBiome)|0xFF000000);
} else {
//Populate the list of biomes for the model state
}
//Note: if the layer isSolid then need to fill all the points in the texture where alpha == 0 with the average colour
// of the surrounding blocks but only within the computed face size bounds
//TODO
this.putTextures(modelId, textureData);
return modelId;
}
//TODO: add a method to detect biome dependent colours (can do by detecting if getColor is ever called)
// if it is, need to add it to a list and mark it as biome colour dependent or something then the shader
// will either use the uint as an index or a direct colour multiplier
private static int captureColourConstant(BlockColorProvider colorProvider, BlockState state, Biome biome) {
return colorProvider.getColor(state, new BlockRenderView() {
@Override
public float getBrightness(Direction direction, boolean shaded) {
return 0;
}
@Override
public int getLightLevel(LightType type, BlockPos pos) {
return 0;
}
@Override
public LightingProvider getLightingProvider() {
return null;
}
@Override
public int getColor(BlockPos pos, ColorResolver colorResolver) {
return colorResolver.getColor(biome, 0, 0);
}
@Nullable
@Override
public BlockEntity getBlockEntity(BlockPos pos) {
return null;
}
@Override
public BlockState getBlockState(BlockPos pos) {
return state;
}
@Override
public FluidState getFluidState(BlockPos pos) {
return state.getFluidState();
}
@Override
public int getHeight() {
return 0;
}
@Override
public int getBottomY() {
return 0;
}
}, BlockPos.ORIGIN, 0);
}
private static boolean isBiomeDependentColour(BlockColorProvider colorProvider, BlockState state) {
boolean[] biomeDependent = new boolean[1];
colorProvider.getColor(state, new BlockRenderView() {
@Override
public float getBrightness(Direction direction, boolean shaded) {
return 0;
}
@Override
public int getLightLevel(LightType type, BlockPos pos) {
return 0;
}
@Override
public LightingProvider getLightingProvider() {
return null;
}
@Override
public int getColor(BlockPos pos, ColorResolver colorResolver) {
biomeDependent[0] = true;
return 0;
}
@Nullable
@Override
public BlockEntity getBlockEntity(BlockPos pos) {
return null;
}
@Override
public BlockState getBlockState(BlockPos pos) {
return state;
}
@Override
public FluidState getFluidState(BlockPos pos) {
return state.getFluidState();
}
@Override
public int getHeight() {
return 0;
}
@Override
public int getBottomY() {
return 0;
}
}, BlockPos.ORIGIN, 0);
return biomeDependent[0];
}
public static boolean faceExists(long metadata, int face) {
return ((metadata>>(8*face))&0xFF)!=0xFF;
}
public static boolean faceCanBeOccluded(long metadata, int face) {
return ((metadata>>(8*face))&0b100)==0b100;
}
public static boolean faceOccludes(long metadata, int face) {
return faceExists(metadata, face) && ((metadata>>(8*face))&0b1)==0b1;
}
public static boolean isColoured(long metadata) {
//TODO: THIS
return false;
}
public static boolean isDoubleSided(long metadata) {
return ((metadata>>(8*6))&4) != 0;
}
public static boolean isTranslucent(long metadata) {
return ((metadata>>(8*6))&2) != 0;
}
public static boolean containsFluid(long metadata) {
return ((metadata>>(8*6))&8) != 0;
}
public static boolean isBiomeColoured(long metadata) {
return ((metadata>>(8*6))&1) != 0;
}
private float[] computeModelDepth(ColourDepthTextureData[] textures, int checkMode) {
float[] res = new float[6];
for (var dir : Direction.values()) {
var data = textures[dir.getId()];
float fd = TextureUtils.computeDepth(data, TextureUtils.DEPTH_MODE_MIN, checkMode);//Compute the min float depth, smaller means closer to the camera, range 0-1
int depth = Math.round(fd * this.modelTextureSize);
//If fd is -1, it means that there was nothing rendered on that face and it should be discarded
if (fd < -0.1) {
res[dir.ordinal()] = -1;
} else {
res[dir.ordinal()] = ((float) depth)/this.modelTextureSize;
}
}
return res;
}
//TODO:FIXME: if the model is not already in the cache for some reason it renders black, need to figure out why
public long getModelMetadata(int blockId) {
int map = 0;
while ((map = this.idMappings[blockId]) == -1) {
Thread.onSpinWait();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (map == -1) {
throw new IllegalArgumentException("Id hasnt been computed yet: " + blockId);
}
long meta = 0;
while ((meta = this.metadataCache[map]) == 0) {
Thread.onSpinWait();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
return meta;
}
public int getModelId(int blockId) {
int map = this.idMappings[blockId];
if (map == -1) {
throw new IllegalArgumentException("Id hasnt been computed yet: " + blockId);
}
return map;
}
private void putTextures(int id, ColourDepthTextureData[] textures) {
int X = (id&0xFF) * this.modelTextureSize*3;
int Y = ((id>>8)&0xFF) * this.modelTextureSize*2;
for (int subTex = 0; subTex < 6; subTex++) {
int x = X + (subTex%3)*this.modelTextureSize;
int y = Y + (subTex/3)*this.modelTextureSize;
GlStateManager._pixelStore(GlConst.GL_UNPACK_ROW_LENGTH, 0);
GlStateManager._pixelStore(GlConst.GL_UNPACK_SKIP_PIXELS, 0);
GlStateManager._pixelStore(GlConst.GL_UNPACK_SKIP_ROWS, 0);
GlStateManager._pixelStore(GlConst.GL_UNPACK_ALIGNMENT, 4);
glTextureSubImage2D(this.textures.id, 0, x, y, this.modelTextureSize, this.modelTextureSize, GL_RGBA, GL_UNSIGNED_BYTE, textures[subTex].colour());
}
}
public int getBufferId() {
return this.modelBuffer.id;
}
public int getTextureId() {
return this.textures.id;
}
public int getSamplerId() {
return this.blockSampler;
}
public void free() {
this.bakery.free();
this.modelBuffer.free();
this.textures.free();
glDeleteSamplers(this.blockSampler);
}
public void addDebugInfo(List<String> info) {
info.add("BlockModels registered: " + this.modelTexture2id.size() + "/" + (1<<16));
}
}

View File

@@ -0,0 +1,269 @@
package me.cortex.zenith.client.core.model;
import com.mojang.blaze3d.platform.GlConst;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.systems.VertexSorter;
import me.cortex.zenith.client.core.gl.GlFramebuffer;
import me.cortex.zenith.client.core.gl.GlTexture;
import me.cortex.zenith.client.core.gl.shader.Shader;
import me.cortex.zenith.client.core.gl.shader.ShaderLoader;
import me.cortex.zenith.client.core.gl.shader.ShaderType;
import me.jellysquid.mods.sodium.client.gl.shader.GlShader;
import net.minecraft.block.*;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gl.GlUniform;
import net.minecraft.client.render.*;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.fluid.FluidState;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.RotationAxis;
import net.minecraft.util.math.random.LocalRandom;
import net.minecraft.world.BlockRenderView;
import net.minecraft.world.LightType;
import net.minecraft.world.biome.ColorResolver;
import net.minecraft.world.chunk.light.LightingProvider;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11C;
import java.util.ArrayList;
import java.util.List;
import static org.lwjgl.opengl.ARBFramebufferObject.*;
import static org.lwjgl.opengl.ARBShaderImageLoadStore.GL_FRAMEBUFFER_BARRIER_BIT;
import static org.lwjgl.opengl.ARBShaderImageLoadStore.glMemoryBarrier;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL14C.glBlendFuncSeparate;
import static org.lwjgl.opengl.GL20C.glUniformMatrix4fv;
import static org.lwjgl.opengl.GL20C.glUseProgram;
import static org.lwjgl.opengl.GL45C.glBlitNamedFramebuffer;
import static org.lwjgl.opengl.GL45C.glGetTextureImage;
//Builds a texture for each face of a model
public class ModelTextureBakery {
private final int width;
private final int height;
private final GlTexture colourTex;
private final GlTexture depthTex;
private final GlFramebuffer framebuffer;
private final Shader rasterShader = Shader.make()
.add(ShaderType.VERTEX, "zenith:bakery/position_tex.vsh")
.add(ShaderType.FRAGMENT, "zenith:bakery/position_tex.fsh")
.compile();
private static final List<MatrixStack> FACE_VIEWS = new ArrayList<>();
public ModelTextureBakery(int width, int height) {
this.width = width;
this.height = height;
this.colourTex = new GlTexture().store(GL_RGBA8, 1, width, height);
this.depthTex = new GlTexture().store(GL_DEPTH24_STENCIL8, 1, width, height);
this.framebuffer = new GlFramebuffer().bind(GL_COLOR_ATTACHMENT0, this.colourTex).bind(GL_DEPTH_STENCIL_ATTACHMENT, this.depthTex).verify();
//This is done to help make debugging easier
FACE_VIEWS.clear();
AddViews();
}
private static void AddViews() {
addView(-90,0, 0, false);//Direction.DOWN
addView(90,0, 0, false);//Direction.UP
addView(0,180, 0, true);//Direction.NORTH
addView(0,0, 0, false);//Direction.SOUTH
//TODO: check these arnt the wrong way round
addView(0,90, 270, false);//Direction.EAST
addView(0,270, 270, false);//Direction.WEST
}
private static void addView(float pitch, float yaw, float rotation, boolean flipX) {
var stack = new MatrixStack();
stack.translate(0.5f,0.5f,0.5f);
stack.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(rotation));
stack.multiply(RotationAxis.POSITIVE_X.rotationDegrees(pitch));
stack.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yaw));
stack.translate(-0.5f,-0.5f,-0.5f);
FACE_VIEWS.add(stack);
}
//TODO: For block entities, also somehow attempt to render the default block entity, e.g. chests and stuff
// cause that will result in ok looking micro details in the terrain
public ColourDepthTextureData[] renderFaces(BlockState state, long randomValue, boolean renderFluid) {
var model = MinecraftClient.getInstance()
.getBakedModelManager()
.getBlockModels()
.getModel(state);
int oldFB = GlStateManager.getBoundFramebuffer();
var oldProjection = new Matrix4f(RenderSystem.getProjectionMatrix());
GL11C.glViewport(0, 0, this.width, this.height);
RenderSystem.setProjectionMatrix(new Matrix4f().identity().set(new float[]{
2,0,0,0,
0, 2,0,0,
0,0, -1f,0,
-1,-1,0,1,
}), VertexSorter.BY_Z);
glClearColor(0,0,0,0);
glClearDepth(1);
glBindFramebuffer(GL_FRAMEBUFFER, this.framebuffer.id);
RenderLayer renderLayer = null;
if (!renderFluid) {
renderLayer = RenderLayers.getBlockLayer(state);
} else {
renderLayer = RenderLayers.getFluidLayer(state.getFluidState());
}
renderLayer.startDrawing();
glEnable(GL_STENCIL_TEST);
glDepthRange(0, 1);
RenderSystem.depthMask(true);
RenderSystem.enableBlend();
RenderSystem.enableDepthTest();
RenderSystem.enableCull();
RenderSystem.depthFunc(GL_LESS);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glStencilOp(GL_KEEP, GL_KEEP, GL_INCR);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
this.rasterShader.bind();
RenderSystem.bindTexture(RenderSystem.getShaderTexture(0));
GlUniform.uniform1(0, 0);
RenderSystem.activeTexture(GlConst.GL_TEXTURE0);
var faces = new ColourDepthTextureData[FACE_VIEWS.size()];
for (int i = 0; i < faces.length; i++) {
faces[i] = captureView(state, model, FACE_VIEWS.get(i), randomValue, i, renderFluid);
//glBlitNamedFramebuffer(this.framebuffer.id, oldFB, 0,0,16,16,300*(i%3),300*(i/3),300*(i%3)+256,300*(i/3)+256, GL_COLOR_BUFFER_BIT, GL_NEAREST);
}
renderLayer.endDrawing();
glDisable(GL_STENCIL_TEST);
RenderSystem.setProjectionMatrix(oldProjection, VertexSorter.BY_DISTANCE);
glBindFramebuffer(GL_FRAMEBUFFER, oldFB);
GL11C.glViewport(GlStateManager.Viewport.getX(), GlStateManager.Viewport.getY(), GlStateManager.Viewport.getWidth(), GlStateManager.Viewport.getHeight());
//TODO: FIXME: fully revert the state of opengl
return faces;
}
private ColourDepthTextureData captureView(BlockState state, BakedModel model, MatrixStack stack, long randomValue, int face, boolean renderFluid) {
var vc = Tessellator.getInstance().getBuffer();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
vc.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_TEXTURE);
if (!renderFluid) {
renderQuads(vc, state, model, new MatrixStack(), randomValue);
} else {
MinecraftClient.getInstance().getBlockRenderManager().renderFluid(BlockPos.ORIGIN, new BlockRenderView() {
@Override
public float getBrightness(Direction direction, boolean shaded) {
return 0;
}
@Override
public LightingProvider getLightingProvider() {
return null;
}
@Override
public int getLightLevel(LightType type, BlockPos pos) {
return 0;
}
@Override
public int getColor(BlockPos pos, ColorResolver colorResolver) {
return 0;
}
@Nullable
@Override
public BlockEntity getBlockEntity(BlockPos pos) {
return null;
}
//TODO: make it so it returns air on some positions, e.g. so from UP,
@Override
public BlockState getBlockState(BlockPos pos) {
//TODO:FIXME: Dont hardcode
if (pos.equals(Direction.byId(face).getVector())) {
return Blocks.AIR.getDefaultState();
}
return state;
}
@Override
public FluidState getFluidState(BlockPos pos) {
if (pos.equals(Direction.byId(face).getVector())) {
return Blocks.AIR.getDefaultState().getFluidState();
}
return state.getFluidState();
}
@Override
public int getHeight() {
return 0;
}
@Override
public int getBottomY() {
return 0;
}
}, vc, state, state.getFluidState());
}
float[] mat = new float[4*4];
new Matrix4f(RenderSystem.getProjectionMatrix()).mul(stack.peek().getPositionMatrix()).get(mat);
glUniformMatrix4fv(1, false, mat);
BufferRenderer.draw(vc.end());
if (state.hasBlockEntity()) {
//TODO: finish BlockEntity raster
//var entity = ((BlockEntityProvider)state).createBlockEntity(BlockPos.ORIGIN, state);
//var renderer = MinecraftClient.getInstance().getBlockEntityRenderDispatcher().get(entity);
//renderer.render();
//entity.markRemoved();
}
glMemoryBarrier(GL_FRAMEBUFFER_BARRIER_BIT);
int[] colourData = new int[this.width*this.height];
int[] depthData = new int[this.width*this.height];
glGetTextureImage(this.colourTex.id, 0, GL_RGBA, GL_UNSIGNED_BYTE, colourData);
glGetTextureImage(this.depthTex.id, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, depthData);
return new ColourDepthTextureData(colourData, depthData, this.width, this.height);
}
private static void renderQuads(BufferBuilder builder, BlockState state, BakedModel model, MatrixStack stack, long randomValue) {
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
var quads = model.getQuads(state, direction, new LocalRandom(randomValue));
for (var quad : quads) {
builder.quad(stack.peek(), quad, 0, 0, 0, 0, 0);
}
}
}
public void free() {
this.framebuffer.free();
this.colourTex.free();
this.depthTex.free();
this.rasterShader.free();
}
}

View File

@@ -0,0 +1,161 @@
package me.cortex.zenith.client.core.model;
import java.util.Map;
//Texturing utils to manipulate data from the model bakery
public class TextureUtils {
//Returns the number of non pixels not written to
public static int getWrittenPixelCount(ColourDepthTextureData texture, int checkMode) {
int count = 0;
for (int i = 0; i < texture.colour().length; i++) {
count += wasPixelWritten(texture, checkMode, i)?1:0;
}
return count;
}
public static boolean isSolid(ColourDepthTextureData texture) {
for (int pixel : texture.colour()) {
if (((pixel>>24)&0xFF) != 255) {
return false;
}
}
return true;
}
public static final int WRITE_CHECK_STENCIL = 1;
public static final int WRITE_CHECK_DEPTH = 2;
public static final int WRITE_CHECK_ALPHA = 3;
private static boolean wasPixelWritten(ColourDepthTextureData data, int mode, int index) {
if (mode == WRITE_CHECK_STENCIL) {
return (data.depth()[index]&0xFF)!=0;
} else if (mode == WRITE_CHECK_DEPTH) {
return (data.depth()[index]>>>8)!=((1<<24)-1);
} else if (mode == WRITE_CHECK_ALPHA) {
return ((data.colour()[index]>>>24)&0xff)!=0;
}
throw new IllegalArgumentException();
}
public static final int DEPTH_MODE_AVG = 1;
public static final int DEPTH_MODE_MAX = 2;
public static final int DEPTH_MODE_MIN = 3;
//Computes depth info based on written pixel data
public static float computeDepth(ColourDepthTextureData texture, int mode, int checkMode) {
final var colourData = texture.colour();
final var depthData = texture.depth();
long a = 0;
long b = 0;
if (mode == DEPTH_MODE_MIN) {
a = Long.MAX_VALUE;
}
if (mode == DEPTH_MODE_MAX) {
a = Long.MIN_VALUE;
}
for (int i = 0; i < colourData.length; i++) {
if (!wasPixelWritten(texture, checkMode, i)) {
continue;
}
int depth = depthData[i]>>>8;
if (mode == DEPTH_MODE_AVG) {
a++;
b += depth;
} else if (mode == DEPTH_MODE_MAX) {
a = Math.max(a, depth);
} else if (mode == DEPTH_MODE_MIN) {
a = Math.min(a, depth);
}
}
if (mode == DEPTH_MODE_AVG) {
if (a == 0) {
return -1;
}
return u2fdepth((int) (b/a));
} else if (mode == DEPTH_MODE_MAX) {
if (a == Long.MIN_VALUE) {
return -1;
}
return u2fdepth((int) a);
} else if (mode == DEPTH_MODE_MIN) {
if (a == Long.MAX_VALUE) {
return -1;
}
return u2fdepth((int) a);
}
throw new IllegalArgumentException();
}
private static float u2fdepth(int depth) {
float depthF = (float) ((double)depth/((1<<24)-1));
//https://registry.khronos.org/OpenGL-Refpages/gl4/html/glDepthRange.xhtml
// due to this and the unsigned bullshit, i believe the depth value needs to get multiplied by 2
depthF *= 2;
if (depthF > 1.00001f) {
throw new IllegalArgumentException("Depth greater than 1");
}
return depthF;
}
//NOTE: data goes from bottom left to top right (x first then y)
public static int[] computeBounds(ColourDepthTextureData data, int checkMode) {
final var depth = data.depth();
//Compute x bounds first
int minX = 0;
minXCheck:
do {
for (int y = 0; y < data.height(); y++) {
int idx = minX + (y * data.width());
if (wasPixelWritten(data, checkMode, idx)) {
break minXCheck;//pixel was written too so break from loop
}
}
minX++;
} while (minX != data.width());
int maxX = data.width()-1;
maxXCheck:
do {
for (int y = data.height()-1; y!=-1; y--) {
int idx = maxX + (y * data.width());
if (wasPixelWritten(data, checkMode, idx)) {
break maxXCheck;//pixel was written too so break from loop
}
}
maxX--;
} while (maxX != -1);
//maxX++;
//Compute y bounds
int minY = 0;
minYCheck:
do {
for (int x = 0; x < data.width(); x++) {
int idx = (minY * data.height()) + x;
if (wasPixelWritten(data, checkMode, idx)) {
break minYCheck;//pixel was written too
}
}
minY++;
} while (minY != data.height());
int maxY = data.height()-1;
maxYCheck:
do {
for (int x = data.width()-1; x!=-1; x--) {
int idx = (maxY * data.height()) + x;
if (wasPixelWritten(data, checkMode, idx)) {
break maxYCheck;//pixel was written too so break from loop
}
}
maxY--;
} while (maxY != -1);
//maxY++;
return new int[]{minX, maxX, minY, maxY};
}
}

View File

@@ -1,4 +0,0 @@
package me.cortex.zenith.client.core.other;
public record BiomeColour(int id, int foliageColour, int waterColour) {
}

View File

@@ -1,4 +0,0 @@
package me.cortex.zenith.client.core.other;
public record BlockStateColour(int id, int biomeTintMsk, int[] faceColours) {
}

View File

@@ -1,138 +0,0 @@
package me.cortex.zenith.client.core.other;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.model.BakedQuad;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.LocalRandom;
public class ColourResolver {
//TODO: sample from multiple random values and avg it
public static int[] resolveColour(BlockState state) {
return resolveColour(state, 1234567890L);
}
//The way this works is it takes the and computes its colour, it then computes the area of the quad and the normal direction
// it adds each area and colour to a per direcition colour
// for non specific axis dimensions it takes the normal of each quad computes the dot between it and each of the directions
// and averages that
// if the colour doesnt exist for a specific axis set it to the average of the other axis and or make it translucent
//TODO: fixme: finish
public static int[] resolveColour(BlockState state, long randomValue) {
if (state == Blocks.AIR.getDefaultState()) {
return new int[6];
}
int[][] builder = new int[6][5];
var random = new LocalRandom(randomValue);
if (state.getFluidState().isEmpty()) {
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
var quads = MinecraftClient.getInstance()
.getBakedModelManager()
.getBlockModels()
.getModel(state)
.getQuads(state, direction, random);
for (var quad : quads) {
long weightColour = resolveQuadColour(quad);
int colour = (int) weightColour;
int weight = (int) (weightColour>>32);
if (direction == null) {
//TODO: apply normal multiplication to weight
for (int i = 0; i < 6; i++) {
builder[i][0] += weight;
builder[i][4] += weight * ((colour>>>24)&0xFF);
builder[i][3] += weight * ((colour>>>16)&0xFF);
builder[i][2] += weight * ((colour>>>8)&0xFF);
builder[i][1] += weight * ((colour>>>0)&0xFF);
}
} else {
builder[direction.getId()][0] += weight;
builder[direction.getId()][4] += weight*((colour>>>24)&0xFF);
builder[direction.getId()][3] += weight*((colour>>>16)&0xFF);
builder[direction.getId()][2] += weight*((colour>>>8)&0xFF);
builder[direction.getId()][1] += weight*((colour>>>0)&0xFF);
}
}
}
} else {
//TODO FIXME: need to account for both the fluid and block state at the same time
//FIXME: make it not hacky and use the fluid handler thing from fabric
long weightColour = resolveNI(MinecraftClient.getInstance().getBakedModelManager().getBlockModels().getModelParticleSprite(state).getContents().image);
for (int i = 0; i < 6; i++) {
builder[i][0] = 1;
builder[i][1] += (weightColour>>0)&0xFF;
builder[i][2] += (weightColour>>8)&0xFF;
builder[i][3] += (weightColour>>16)&0xFF;
builder[i][4] += (weightColour>>24)&0xFF;
}
}
int[] out = new int[6];
for (int i = 0; i < 6; i++) {
int c = builder[i][0];
if (c == 0) {
continue;
}
int r = builder[i][4]/c;
int g = builder[i][3]/c;
int b = builder[i][2]/c;
int a = builder[i][1]/c;
out[i] = (r<<24)|(g<<16)|(b<<8)|a;
}
return out;
}
private static long resolveQuadColour(BakedQuad quad) {
return resolveNI(quad.getSprite().getContents().image);
}
private static long resolveNI(NativeImage image) {
int r = 0;
int g = 0;
int b = 0;
int a = 0;
int count = 0;
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int colour = image.getColor(x, y);
if (((colour >>> 24)&0xFF) == 0) {
continue;
}
r += (colour >>> 0) & 0xFF;
g += (colour >>> 8) & 0xFF;
b += (colour >>> 16) & 0xFF;
a += (colour >>> 24) & 0xFF;
count++;
}
}
if (count == 0) {
return 0;
}
r /= count;
g /= count;
b /= count;
a /= count;
int colour = (r<<24)|(g<<16)|(b<<8)|a;
return Integer.toUnsignedLong(colour)|(((long)count)<<32);
}
public static long resolveBiomeColour(String biomeId) {
var biome = MinecraftClient.getInstance().world.getRegistryManager().get(RegistryKeys.BIOME).get(new Identifier(biomeId));
if (biome == null) {
System.err.println("Biome: " + biomeId + " doesnt exist in registry!");
return 0;
}
int ARGBFoliage = biome.getFoliageColor();
int ARGBWater = biome.getWaterColor();
return Integer.toUnsignedLong(((ARGBFoliage&0xFFFFFF)<<8)|(ARGBFoliage>>>24)) | (Integer.toUnsignedLong(((ARGBWater&0xFFFFFF)<<8)|(ARGBWater>>>24))<<32);
}
}

View File

@@ -4,10 +4,11 @@ package me.cortex.zenith.client.core.rendering;
// could maybe tosomething else
import me.cortex.zenith.client.core.gl.GlBuffer;
import me.cortex.zenith.client.core.rendering.building.BuiltSectionGeometry;
import me.cortex.zenith.client.core.model.ModelManager;
import me.cortex.zenith.client.core.rendering.building.BuiltSection;
import me.cortex.zenith.client.core.rendering.util.UploadStream;
import me.cortex.zenith.client.core.other.BiomeColour;
import me.cortex.zenith.client.core.other.BlockStateColour;
import me.cortex.zenith.common.world.other.Mapper;
import net.minecraft.block.BlockState;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.Camera;
import net.minecraft.client.render.Frustum;
@@ -16,6 +17,7 @@ import org.joml.FrustumIntersection;
import org.lwjgl.system.MemoryUtil;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;
import static org.lwjgl.opengl.ARBMultiDrawIndirect.glMultiDrawElementsIndirect;
@@ -35,14 +37,9 @@ public abstract class AbstractFarWorldRenderer {
protected final GlBuffer uniformBuffer;
protected final GeometryManager geometry;
private final ConcurrentLinkedDeque<BlockStateColour> stateUpdateQueue = new ConcurrentLinkedDeque<>();
private final ConcurrentLinkedDeque<BiomeColour> biomeUpdateQueue = new ConcurrentLinkedDeque<>();
protected final GlBuffer stateDataBuffer;
protected final GlBuffer biomeDataBuffer;
protected final ModelManager models;
protected final GlBuffer lightDataBuffer;
//Current camera base level section position
protected int sx;
protected int sy;
@@ -50,13 +47,12 @@ public abstract class AbstractFarWorldRenderer {
protected FrustumIntersection frustum;
public AbstractFarWorldRenderer() {
this.uniformBuffer = new GlBuffer(1024, 0);
//TODO: make these both dynamically sized
this.stateDataBuffer = new GlBuffer((1<<16)*28, 0);//Capacity for 1<<16 entries
this.biomeDataBuffer = new GlBuffer(512*4*2, 0);//capacity for 1<<9 entries
this.lightDataBuffer = new GlBuffer(256*4, 0);//256 of uint
this.geometry = new GeometryManager();
private final ConcurrentLinkedDeque<Mapper.StateEntry> blockStateUpdates = new ConcurrentLinkedDeque<>();
public AbstractFarWorldRenderer(int geometrySize, int maxSections) {
this.uniformBuffer = new GlBuffer(1024);
this.lightDataBuffer = new GlBuffer(256*4);//256 of uint
this.geometry = new GeometryManager(geometrySize*8L, maxSections);
this.models = new ModelManager(16);
}
protected abstract void setupVao();
@@ -68,7 +64,6 @@ public abstract class AbstractFarWorldRenderer {
this.sy = camera.getBlockPos().getY() >> 5;
this.sz = camera.getBlockPos().getZ() >> 5;
//TODO: move this to a render function that is only called
// once per frame when using multi viewport mods
//it shouldent matter if its called multiple times a frame however, as its synced with fences
@@ -90,49 +85,38 @@ public abstract class AbstractFarWorldRenderer {
//Upload any new geometry
this.geometry.uploadResults();
//Upload any block state changes
while (!this.stateUpdateQueue.isEmpty()) {
var stateUpdate = this.stateUpdateQueue.pop();
long ptr = UploadStream.INSTANCE.upload(this.stateDataBuffer, stateUpdate.id()*28L, 28);
MemoryUtil.memPutInt(ptr, stateUpdate.biomeTintMsk()); ptr+=4;
for (int faceColour : stateUpdate.faceColours()) {
MemoryUtil.memPutInt(ptr, faceColour); ptr+=4;
}
}
//Upload any biome changes
while (!this.biomeUpdateQueue.isEmpty()) {
var biomeUpdate = this.biomeUpdateQueue.pop();
long ptr = UploadStream.INSTANCE.upload(this.biomeDataBuffer, biomeUpdate.id()*8L, 8);
MemoryUtil.memPutInt(ptr, biomeUpdate.foliageColour()); ptr+=4;
MemoryUtil.memPutInt(ptr, biomeUpdate.waterColour()); ptr+=4;
//Do any BlockChanges
while (!this.blockStateUpdates.isEmpty()) {
var update = this.blockStateUpdates.pop();
this.models.addEntry(update.id, update.state);
}
}
public abstract void renderFarAwayOpaque(MatrixStack stack, double cx, double cy, double cz);
public void enqueueUpdate(BlockStateColour stateColour) {
this.stateUpdateQueue.add(stateColour);
}
public abstract void renderFarAwayTranslucent();
public void enqueueUpdate(BiomeColour biomeColour) {
this.biomeUpdateQueue.add(biomeColour);
}
public void enqueueResult(BuiltSectionGeometry result) {
public void enqueueResult(BuiltSection result) {
this.geometry.enqueueResult(result);
}
public void addDebugData(List<String> debug) {
public void addBlockState(Mapper.StateEntry entry) {
this.blockStateUpdates.add(entry);
}
public void addDebugData(List<String> debug) {
this.models.addDebugInfo(debug);
}
public void shutdown() {
glDeleteVertexArrays(this.vao);
this.models.free();
this.geometry.free();
this.uniformBuffer.free();
this.stateDataBuffer.free();
this.biomeDataBuffer.free();
this.lightDataBuffer.free();
}
public ModelManager getModelManager() {
return this.models;
}
}

View File

@@ -4,7 +4,7 @@ import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.cortex.zenith.client.core.gl.GlBuffer;
import me.cortex.zenith.client.core.rendering.building.BuiltSectionGeometry;
import me.cortex.zenith.client.core.rendering.building.BuiltSection;
import me.cortex.zenith.client.core.rendering.util.BufferArena;
import me.cortex.zenith.client.core.rendering.util.UploadStream;
import org.lwjgl.system.MemoryUtil;
@@ -14,21 +14,7 @@ import java.util.concurrent.ConcurrentLinkedDeque;
public class GeometryManager {
private static final int SECTION_METADATA_SIZE = 32;
private record SectionMeta(long position, long opaqueGeometryPtr, int opaqueQuadCount, long translucentGeometryPtr, int translucentQuadCount) {
public void writeMetadata(long ptr) {
//THIS IS DUE TO ENDIANNESS and that we are splitting a long into 2 ints
MemoryUtil.memPutInt(ptr, (int) (this.position>>32)); ptr += 4;
MemoryUtil.memPutInt(ptr, (int) this.position); ptr += 4;
MemoryUtil.memPutInt(ptr, (int) this.opaqueGeometryPtr); ptr += 4;
MemoryUtil.memPutInt(ptr, this.opaqueQuadCount); ptr += 4;
}
}
private final ConcurrentLinkedDeque<BuiltSectionGeometry> buildResults = new ConcurrentLinkedDeque<>();
private final ConcurrentLinkedDeque<BuiltSection> buildResults = new ConcurrentLinkedDeque<>();
private int sectionCount = 0;
private final Long2IntOpenHashMap pos2id = new Long2IntOpenHashMap();
private final LongArrayList id2pos = new LongArrayList();
@@ -37,37 +23,16 @@ public class GeometryManager {
private final GlBuffer sectionMetaBuffer;
private final BufferArena geometryBuffer;
public GeometryManager() {
this.sectionMetaBuffer = new GlBuffer(1L << 23, 0);
this.geometryBuffer = new BufferArena((1L << 31) - 1024, 8);
public GeometryManager(long geometryBufferSize, int maxSections) {
this.sectionMetaBuffer = new GlBuffer(((long) maxSections) * SECTION_METADATA_SIZE);
this.geometryBuffer = new BufferArena(geometryBufferSize, 8);
this.pos2id.defaultReturnValue(-1);
}
public void enqueueResult(BuiltSectionGeometry sectionGeometry) {
this.buildResults.add(sectionGeometry);
}
private SectionMeta createMeta(BuiltSectionGeometry geometry) {
long geometryPtr = this.geometryBuffer.upload(geometry.geometryBuffer);
//TODO: support translucent geometry
return new SectionMeta(geometry.position, geometryPtr, (int) (geometry.geometryBuffer.size/8), -1,0);
}
private void freeMeta(SectionMeta meta) {
if (meta.opaqueGeometryPtr != -1) {
this.geometryBuffer.free(meta.opaqueGeometryPtr);
}
if (meta.translucentGeometryPtr != -1) {
this.geometryBuffer.free(meta.translucentGeometryPtr);
}
}
void uploadResults() {
while (!this.buildResults.isEmpty()) {
var result = this.buildResults.pop();
boolean isDelete = result.geometryBuffer == null && result.translucentGeometryBuffer == null;
boolean isDelete = result.geometryBuffer == null;
if (isDelete) {
int id = -1;
if ((id = this.pos2id.remove(result.position)) != -1) {
@@ -140,6 +105,10 @@ public class GeometryManager {
}
}
public void enqueueResult(BuiltSection sectionGeometry) {
this.buildResults.add(sectionGeometry);
}
public int getSectionCount() {
return this.sectionCount;
}
@@ -160,9 +129,42 @@ public class GeometryManager {
return this.sectionMetaBuffer.id;
}
public float getGeometryBufferUsage() {
return this.geometryBuffer.usage();
}
//=========================================================================================================================================================================================
//=========================================================================================================================================================================================
//=========================================================================================================================================================================================
//TODO: pack the offsets of each axis so that implicit face culling can work
//Note! the opaquePreDataCount and translucentPreDataCount are never writen to the meta buffer, as they are indexed in reverse relative to the base opaque and translucent geometry
private record SectionMeta(long position, int aabb, int geometryPtr, int size, int[] offsets) {
public void writeMetadata(long ptr) {
//THIS IS DUE TO ENDIANNESS and that we are splitting a long into 2 ints
MemoryUtil.memPutInt(ptr, (int) (this.position>>32)); ptr += 4;
MemoryUtil.memPutInt(ptr, (int) this.position); ptr += 4;
MemoryUtil.memPutInt(ptr, (int) this.aabb); ptr += 4;
MemoryUtil.memPutInt(ptr, this.geometryPtr + this.offsets[0]); ptr += 4;
MemoryUtil.memPutInt(ptr, (this.offsets[1]-this.offsets[0])|((this.offsets[2]-this.offsets[1])<<16)); ptr += 4;
MemoryUtil.memPutInt(ptr, (this.offsets[3]-this.offsets[2])|((this.offsets[4]-this.offsets[3])<<16)); ptr += 4;
MemoryUtil.memPutInt(ptr, (this.offsets[5]-this.offsets[4])|((this.offsets[6]-this.offsets[5])<<16)); ptr += 4;
MemoryUtil.memPutInt(ptr, (this.offsets[7]-this.offsets[6])|((this.size -this.offsets[7])<<16)); ptr += 4;
}
}
private SectionMeta createMeta(BuiltSection geometry) {
int geometryPtr = (int) this.geometryBuffer.upload(geometry.geometryBuffer);
return new SectionMeta(geometry.position, geometry.aabb, geometryPtr, (int) (geometry.geometryBuffer.size/8), geometry.offsets);
}
private void freeMeta(SectionMeta meta) {
if (meta.geometryPtr != -1) {
this.geometryBuffer.free(meta.geometryPtr);
}
}
}

View File

@@ -1,29 +1,41 @@
package me.cortex.zenith.client.core.rendering;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import me.cortex.zenith.client.core.gl.GlBuffer;
import me.cortex.zenith.client.core.gl.shader.Shader;
import me.cortex.zenith.client.core.gl.shader.ShaderType;
import me.cortex.zenith.client.core.rendering.util.UploadStream;
import me.cortex.zenith.client.mixin.joml.AccessFrustumIntersection;
import net.minecraft.block.Blocks;
import net.minecraft.block.WallMountedBlock;
import net.minecraft.block.enums.BlockFace;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.Direction;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.lwjgl.opengl.ARBIndirectParameters;
import org.lwjgl.opengl.GL11C;
import org.lwjgl.system.MemoryUtil;
import java.util.List;
import static org.lwjgl.opengl.ARBIndirectParameters.GL_PARAMETER_BUFFER_ARB;
import static org.lwjgl.opengl.ARBIndirectParameters.glMultiDrawElementsIndirectCountARB;
import static org.lwjgl.opengl.ARBMultiDrawIndirect.glMultiDrawElementsIndirect;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_SHORT;
import static org.lwjgl.opengl.GL11.glGetInteger;
import static org.lwjgl.opengl.GL30.glBindVertexArray;
import static org.lwjgl.opengl.GL30C.GL_R8UI;
import static org.lwjgl.opengl.GL30C.GL_RED_INTEGER;
import static org.lwjgl.opengl.GL40C.GL_DRAW_INDIRECT_BUFFER;
import static org.lwjgl.opengl.GL42.*;
import static org.lwjgl.opengl.GL42.GL_FRAMEBUFFER_BARRIER_BIT;
import static org.lwjgl.opengl.GL43.*;
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER;
import static org.lwjgl.opengl.NVRepresentativeFragmentTest.GL_REPRESENTATIVE_FRAGMENT_TEST_NV;
import static org.lwjgl.opengl.GL45C.glClearNamedBufferData;
public class Gl46FarWorldRenderer extends AbstractFarWorldRenderer {
private final Shader commandGen = Shader.make()
@@ -42,11 +54,17 @@ public class Gl46FarWorldRenderer extends AbstractFarWorldRenderer {
.add(ShaderType.FRAGMENT, "zenith:lod/gl46/cull/raster.frag")
.compile();
private final GlBuffer glCommandBuffer = new GlBuffer(200_000*5*4, 0);
private final GlBuffer glVisibilityBuffer = new GlBuffer(200_000*4, 0);
private final GlBuffer glCommandBuffer;
private final GlBuffer glCommandCountBuffer;
private final GlBuffer glVisibilityBuffer;
public Gl46FarWorldRenderer() {
super();
public Gl46FarWorldRenderer(int geometryBuffer, int maxSections) {
super(geometryBuffer, maxSections);
this.glCommandBuffer = new GlBuffer(maxSections*5L*4 * 6);
this.glCommandCountBuffer = new GlBuffer(4*2);
this.glVisibilityBuffer = new GlBuffer(maxSections*4L);
glClearNamedBufferData(this.glCommandBuffer.id, GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, new int[1]);
glClearNamedBufferData(this.glVisibilityBuffer.id, GL_R8UI, GL_RED_INTEGER, GL_UNSIGNED_BYTE, new int[1]);
setupVao();
}
@@ -54,75 +72,19 @@ public class Gl46FarWorldRenderer extends AbstractFarWorldRenderer {
protected void setupVao() {
glBindVertexArray(this.vao);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, this.glCommandBuffer.id);
glBindBuffer(GL_PARAMETER_BUFFER_ARB, this.glCommandCountBuffer.id);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE.id());
glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniformBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.geometry.geometryId());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, this.glCommandBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.geometry.metaId());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, this.glVisibilityBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, this.stateDataBuffer.id);//State LUT
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, this.biomeDataBuffer.id);//Biome LUT
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.glCommandCountBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, this.geometry.metaId());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, this.glVisibilityBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, this.models.getBufferId());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 7, this.lightDataBuffer.id);//Lighting LUT
glBindVertexArray(0);
}
public void renderFarAwayOpaque(MatrixStack stack, double cx, double cy, double cz) {
if (this.geometry.getSectionCount() == 0) {
return;
}
RenderLayer.getCutoutMipped().startDrawing();
//RenderSystem.enableBlend();
//RenderSystem.defaultBlendFunc();
this.updateUniformBuffer(stack, cx, cy, cz);
UploadStream.INSTANCE.commit();
glBindVertexArray(this.vao);
this.commandGen.bind();
glDispatchCompute((this.geometry.getSectionCount() + 127) / 128, 1, 1);
glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT);
this.lodShader.bind();
if (false) {//Bloody intel gpus
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, 0, 1000, 0);
//int count = this.geometry.getSectionCount()/1000;
//for (int i = 0; i < 10; i++) {
// glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, i*1000L*20, 1000, 0);
//}
//int rem = this.geometry.getSectionCount() - (count*1000);
//if (rem != 0) {
// glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, count*1000L*20, rem, 0);
//}
} else {
//TODO: swap to a multidraw indirect counted
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, 0, this.geometry.getSectionCount(), 0);
}
//ARBIndirectParameters.glMultiDrawElementsIndirectCountARB(
glMemoryBarrier(GL_PIXEL_BUFFER_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
//TODO: add gpu occlusion culling here (after the lod drawing) (maybe, finish the rest of the PoC first)
cullShader.bind();
glColorMask(false, false, false, false);
glDepthMask(false);
//glEnable(GL_REPRESENTATIVE_FRAGMENT_TEST_NV);
glDrawElementsInstanced(GL_TRIANGLES, 6 * 2 * 3, GL_UNSIGNED_BYTE, (1 << 16) * 6 * 2, this.geometry.getSectionCount());
//glDisable(GL_REPRESENTATIVE_FRAGMENT_TEST_NV);
glDepthMask(true);
glColorMask(true, true, true, true);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
glBindVertexArray(0);
RenderLayer.getCutoutMipped().endDrawing();
}
//FIXME: dont do something like this as it breaks multiviewport mods
private int frameId = 0;
private void updateUniformBuffer(MatrixStack stack, double cx, double cy, double cz) {
@@ -143,6 +105,99 @@ public class Gl46FarWorldRenderer extends AbstractFarWorldRenderer {
MemoryUtil.memPutInt(ptr, this.frameId++); ptr += 4;
}
public void renderFarAwayOpaque(MatrixStack stack, double cx, double cy, double cz) {
if (this.geometry.getSectionCount() == 0) {
return;
}
//this.models.addEntry(0, Blocks.STONE.getDefaultState());
RenderLayer.getCutoutMipped().startDrawing();
int oldActiveTexture = glGetInteger(GL_ACTIVE_TEXTURE);
//RenderSystem.enableBlend();
//RenderSystem.defaultBlendFunc();
this.updateUniformBuffer(stack, cx, cy, cz);
UploadStream.INSTANCE.commit();
glBindVertexArray(this.vao);
//Bind the texture atlas
glActiveTexture(GL_TEXTURE0);
int oldBoundTexture = glGetInteger(GL_TEXTURE_BINDING_2D);
glBindSampler(0, this.models.getSamplerId());
glBindTexture(GL_TEXTURE_2D, this.models.getTextureId());
glClearNamedBufferData(this.glCommandCountBuffer.id, GL_R32UI, GL_RED_INTEGER, GL_UNSIGNED_INT, new int[1]);
this.commandGen.bind();
glDispatchCompute((this.geometry.getSectionCount() + 127) / 128, 1, 1);
glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT | GL_UNIFORM_BARRIER_BIT);
this.lodShader.bind();
glDisable(GL_CULL_FACE);
glMultiDrawElementsIndirectCountARB(GL_TRIANGLES, GL_UNSIGNED_SHORT, 0, 0, (int) (this.geometry.getSectionCount()*4.4), 0);
glEnable(GL_CULL_FACE);
glMemoryBarrier(GL_PIXEL_BUFFER_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);
this.cullShader.bind();
glColorMask(false, false, false, false);
glDepthMask(false);
//glEnable(GL_REPRESENTATIVE_FRAGMENT_TEST_NV);
glDrawElementsInstanced(GL_TRIANGLES, 6 * 2 * 3, GL_UNSIGNED_BYTE, (1 << 16) * 6 * 2, this.geometry.getSectionCount());
//glDisable(GL_REPRESENTATIVE_FRAGMENT_TEST_NV);
glDepthMask(true);
glColorMask(true, true, true, true);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
//TODO: need to do temporal rasterization here
glBindVertexArray(0);
glBindSampler(0, 0);
GL11C.glBindTexture(GL_TEXTURE_2D, oldBoundTexture);
glActiveTexture(oldActiveTexture);
RenderLayer.getCutoutMipped().endDrawing();
}
@Override
public void renderFarAwayTranslucent() {
RenderLayer.getTranslucent().startDrawing();
glBindVertexArray(this.vao);
glDisable(GL_CULL_FACE);
RenderSystem.enableBlend();
RenderSystem.blendFuncSeparate(GlStateManager.SrcFactor.SRC_ALPHA, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SrcFactor.ONE, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA);
int oldActiveTexture = glGetInteger(GL_ACTIVE_TEXTURE);
glBindSampler(0, this.models.getSamplerId());
glActiveTexture(GL_TEXTURE0);
int oldBoundTexture = glGetInteger(GL_TEXTURE_BINDING_2D);
glBindTexture(GL_TEXTURE_2D, this.models.getTextureId());
this.lodShader.bind();
glDepthMask(false);
glMultiDrawElementsIndirectCountARB(GL_TRIANGLES, GL_UNSIGNED_SHORT, 400_000 * 4 * 5, 4, this.geometry.getSectionCount(), 0);
glDepthMask(true);
glEnable(GL_CULL_FACE);
glBindVertexArray(0);
glBindSampler(0, 0);
GL11C.glBindTexture(GL_TEXTURE_2D, oldBoundTexture);
glActiveTexture(oldActiveTexture);
RenderSystem.disableBlend();
RenderLayer.getTranslucent().endDrawing();
}
@Override
public void shutdown() {
super.shutdown();
@@ -151,10 +206,12 @@ public class Gl46FarWorldRenderer extends AbstractFarWorldRenderer {
this.cullShader.free();
this.glCommandBuffer.free();
this.glVisibilityBuffer.free();
this.glCommandCountBuffer.free();
}
@Override
public void addDebugData(List<String> debug) {
super.addDebugData(debug);
debug.add("Geometry buffer usage: " + ((float)Math.round((this.geometry.getGeometryBufferUsage()*100000))/1000) + "%");
debug.add("Render Sections: " + this.geometry.getSectionCount());
}

View File

@@ -1,5 +0,0 @@
package me.cortex.zenith.client.core.rendering;
//Manages the storage and updating of model states, textures and colours
public class ModelManager {
}

View File

@@ -1,50 +0,0 @@
package me.cortex.zenith.client.core.rendering;
import me.cortex.zenith.client.core.gl.shader.Shader;
import me.cortex.zenith.client.core.gl.shader.ShaderType;
import me.cortex.zenith.client.core.rendering.util.UploadStream;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.util.math.MatrixStack;
import static org.lwjgl.opengl.ARBMultiDrawIndirect.glMultiDrawElementsIndirect;
import static org.lwjgl.opengl.GL30.glBindVertexArray;
import static org.lwjgl.opengl.NVMeshShader.glDrawMeshTasksNV;
//TODO: make this a 2 phase culling system
// first phase renders the terrain, in the terrain task shader it also checks if the section was not visible in the frustum but now is
// and then renders it and marks it as being in the frustum
public class NvFarWorldRenderer extends AbstractFarWorldRenderer {
private final Shader primaryTerrainRaster = Shader.make()
.add(ShaderType.TASK, "voxelmon:lod/nvmesh/primary.task")
.add(ShaderType.MESH, "voxelmon:lod/nvmesh/primary.mesh")
.add(ShaderType.FRAGMENT, "voxelmon:lod/nvmesh/primary.frag")
.compile();
@Override
protected void setupVao() {
}
@Override
public void renderFarAwayOpaque(MatrixStack stack, double cx, double cy, double cz) {
if (this.geometry.getSectionCount() == 0) {
return;
}
RenderLayer.getCutoutMipped().startDrawing();
UploadStream.INSTANCE.commit();
glBindVertexArray(this.vao);
this.primaryTerrainRaster.bind();
glDrawMeshTasksNV(0, this.geometry.getSectionCount());
glBindVertexArray(0);
RenderLayer.getCutoutMipped().endDrawing();
}
@Override
public void shutdown() {
super.shutdown();
}
}

View File

@@ -1,6 +1,6 @@
package me.cortex.zenith.client.core.rendering;
import me.cortex.zenith.client.core.rendering.building.BuiltSectionGeometry;
import me.cortex.zenith.client.core.rendering.building.BuiltSection;
import me.cortex.zenith.client.core.rendering.building.RenderGenerationService;
import me.cortex.zenith.common.world.WorldEngine;
import me.cortex.zenith.common.world.WorldSection;
@@ -41,7 +41,7 @@ public class RenderTracker {
//Removes a lvl 0 section from the world renderer
public void remLvl0(int x, int y, int z) {
this.activeSections.remove(WorldEngine.getWorldSectionId(0, x, y, z));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(0, x, y, z), null, null));
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(0, x, y, z)));
this.renderGen.removeTask(0, x, y, z);
}
@@ -63,14 +63,14 @@ public class RenderTracker {
this.renderGen.enqueueTask(lvl, x, y, z, this::shouldStillBuild, this::getBuildFlagsOrAbort);
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1), null, null));
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1))));
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1)));
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1))));
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1)));
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1))));
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1)));
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1))));
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1)));
this.renderGen.removeTask(lvl-1, (x<<1), (y<<1), (z<<1));
@@ -95,7 +95,7 @@ public class RenderTracker {
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1), O);
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl, x, y, z));
this.renderer.enqueueResult(new BuiltSectionGeometry(lvl, x, y, z, null, null));
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl, x, y, z)));
this.renderGen.removeTask(lvl, x, y, z);
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1), this::shouldStillBuild, this::getBuildFlagsOrAbort);
@@ -134,7 +134,7 @@ public class RenderTracker {
//called by the RenderGenerationService about built geometry, the RenderTracker checks if it can use the result (e.g. the LoD hasnt changed/still correct etc)
// and dispatches it to the renderer
// it also batch collects the geometry sections until all the geometry for an operation is collected, then it executes the operation, its removes flickering
public void processBuildResult(BuiltSectionGeometry section) {
public void processBuildResult(BuiltSection section) {
//Check that we still want the section
if (this.activeSections.containsKey(section.position)) {
this.renderer.enqueueResult(section);

View File

@@ -14,7 +14,7 @@ public class SharedIndexBuffer {
private final GlBuffer indexBuffer;
public SharedIndexBuffer() {
this.indexBuffer = new GlBuffer((1<<16)*6*2 + 6*2*3, 0);
this.indexBuffer = new GlBuffer((1<<16)*6*2 + 6*2*3);
var quadIndexBuff = IndexUtil.generateQuadIndicesShort(16380);
var cubeBuff = generateCubeIndexBuffer();

View File

@@ -0,0 +1,43 @@
package me.cortex.zenith.client.core.rendering.building;
import me.cortex.zenith.common.util.MemoryBuffer;
import java.util.Arrays;
import java.util.Objects;
//TODO: also have an AABB size stored
public final class BuiltSection {
public final long position;
public final int aabb;
public final MemoryBuffer geometryBuffer;
public final int[] offsets;
public BuiltSection(long position) {
this(position, -1, null, null);
}
public BuiltSection(long position, int aabb, MemoryBuffer geometryBuffer, int[] offsets) {
this.position = position;
this.aabb = aabb;
this.geometryBuffer = geometryBuffer;
this.offsets = offsets;
if (offsets != null) {
for (int i = 0; i < offsets.length-1; i++) {
int delta = offsets[i+1] - offsets[i];
if (delta<0||delta>=(1<<16)) {
throw new IllegalArgumentException("Offsets out of range");
}
}
}
}
public BuiltSection clone() {
return new BuiltSection(this.position, this.aabb, this.geometryBuffer!=null?this.geometryBuffer.copy():null, this.offsets!=null?Arrays.copyOf(this.offsets, this.offsets.length):null);
}
public void free() {
if (this.geometryBuffer != null) {
this.geometryBuffer.free();
}
}
}

View File

@@ -1,32 +0,0 @@
package me.cortex.zenith.client.core.rendering.building;
import me.cortex.zenith.common.util.MemoryBuffer;
import me.cortex.zenith.common.world.WorldEngine;
public class BuiltSectionGeometry {
public final long position;
public final MemoryBuffer geometryBuffer;
public final MemoryBuffer translucentGeometryBuffer;
public BuiltSectionGeometry(int lvl, int x, int y, int z, MemoryBuffer geometryBuffer, MemoryBuffer translucentGeometryBuffer) {
this(WorldEngine.getWorldSectionId(lvl, x, y, z), geometryBuffer, translucentGeometryBuffer);
}
public BuiltSectionGeometry(long position, MemoryBuffer geometryBuffer, MemoryBuffer translucentGeometryBuffer) {
this.position = position;
this.geometryBuffer = geometryBuffer;
this.translucentGeometryBuffer = translucentGeometryBuffer;
}
public void free() {
if (this.geometryBuffer != null) {
this.geometryBuffer.free();
}
if (this.translucentGeometryBuffer != null) {
this.translucentGeometryBuffer.free();
}
}
public BuiltSectionGeometry clone() {
return new BuiltSectionGeometry(this.position, this.geometryBuffer!=null?this.geometryBuffer.copy():null, this.translucentGeometryBuffer!=null?this.translucentGeometryBuffer.copy():null);
}
}

View File

@@ -1,6 +1,8 @@
package me.cortex.zenith.client.core.rendering.building;
import me.cortex.zenith.client.core.model.ModelManager;
import me.cortex.zenith.client.core.rendering.GeometryManager;
import me.cortex.zenith.client.core.util.Mesher2D;
import me.cortex.zenith.common.world.other.Mapper;
import net.minecraft.client.color.block.BlockColors;

View File

@@ -1,25 +1,42 @@
package me.cortex.zenith.client.core.rendering.building;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import me.cortex.zenith.common.util.MemoryBuffer;
import me.cortex.zenith.client.core.model.ModelManager;
import me.cortex.zenith.client.core.util.Mesher2D;
import me.cortex.zenith.common.util.MemoryBuffer;
import me.cortex.zenith.common.world.WorldEngine;
import me.cortex.zenith.common.world.WorldSection;
import me.cortex.zenith.common.world.other.Mapper;
import net.minecraft.block.FluidBlock;
import net.minecraft.client.MinecraftClient;
import net.minecraft.util.math.Direction;
import org.lwjgl.system.MemoryUtil;
public class RenderDataFactory {
private final Mesher2D mesher = new Mesher2D(5,15);//15
private final LongArrayList outData = new LongArrayList(1000);
private final WorldEngine world;
private final ModelManager modelMan;
private final QuadEncoder encoder;
private final Mesher2D negativeMesher = new Mesher2D(5, 15);
private final Mesher2D positiveMesher = new Mesher2D(5, 15);
private final long[] sectionCache = new long[32*32*32];
private final long[] connectedSectionCache = new long[32*32*32];
private final QuadEncoder encoder;
public RenderDataFactory(WorldEngine world) {
private final LongArrayList doubleSidedQuadCollector = new LongArrayList();
private final LongArrayList translucentQuadCollector = new LongArrayList();
private final LongArrayList[] directionalQuadCollectors = new LongArrayList[]{new LongArrayList(), new LongArrayList(), new LongArrayList(), new LongArrayList(), new LongArrayList(), new LongArrayList()};
private int minX;
private int minY;
private int minZ;
private int maxX;
private int maxY;
private int maxZ;
public RenderDataFactory(WorldEngine world, ModelManager modelManager) {
this.world = world;
this.modelMan = modelManager;
this.encoder = new QuadEncoder(world.getMapper(), MinecraftClient.getInstance().getBlockColors(), MinecraftClient.getInstance().world);
}
@@ -32,304 +49,492 @@ public class RenderDataFactory {
//buildMask in the lower 6 bits contains the faces to build, the next 6 bits are whether the edge face builds against
// its neigbor or not (0 if it does 1 if it doesnt (0 is default behavior))
public BuiltSectionGeometry generateMesh(WorldSection section, int buildMask) {
//TODO: to speed it up more, check like section.isEmpty() and stuff like that, have masks for if a slice/layer is entirly air etc
//TODO: instead of having it check its neighbors with the same lod level, compare against 1 level lower, this will prevent cracks and seams from
// appearing between lods
//if (section.definitelyEmpty()) {//Fast path if its known the entire chunk is empty
// return new BuiltSectionGeometry(section.getKey(), null, null);
//}
public BuiltSection generateMesh(WorldSection section, int buildMask) {
section.copyDataTo(this.sectionCache);
var data = this.sectionCache;
this.translucentQuadCollector.clear();
this.doubleSidedQuadCollector.clear();
for (var collector : this.directionalQuadCollectors) {
collector.clear();
}
this.minX = Integer.MAX_VALUE;
this.minY = Integer.MAX_VALUE;
this.minZ = Integer.MAX_VALUE;
this.maxX = Integer.MIN_VALUE;
this.maxY = Integer.MIN_VALUE;
this.maxZ = Integer.MIN_VALUE;
long[] connectedData = null;
int dirId = Direction.UP.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int y = 0; y < 32; y++) {
this.mesher.reset();
//TODO:NOTE! when doing face culling of translucent blocks,
// if the connecting type of the translucent block is the same AND the face is full, discard it
// this stops e.g. multiple layers of glass (and ocean) from having 3000 layers of quads etc
for (int z = 0; z < 32; z++) {
for (int x = 0; x < 32; x++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) {
continue;
}
long up = -1;
if (y < 31) {
up = data[WorldSection.getIndex(x, y + 1, z)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
if (y == 31 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.acquire(section.lvl, section.x, section.y + 1, section.z);
connectedSection.copyDataTo(this.connectedSectionCache);
connectedData = this.connectedSectionCache;
connectedSection.release();
}
up = connectedData[WorldSection.getIndex(x, 0, z)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
//Recodes the id to include the correct lighting
this.mesher.put(x, z, (self&~(0xFFL<<56))|(up&(0xFFL<<56)));
}
}
var count = this.mesher.process();
var array = this.mesher.getArray();
for (int i = 0; i < count; i++) {
var quad = array[i];
this.outData.add(this.encoder.encode(this.mesher.getDataFromQuad(quad), 1, y, quad));
}
this.generateMeshForAxis(section, 0);//Direction.Axis.Y
this.generateMeshForAxis(section, 1);//Direction.Axis.Z
this.generateMeshForAxis(section, 2);//Direction.Axis.X
int quadCount = this.doubleSidedQuadCollector.size() + this.translucentQuadCollector.size();
for (var collector : this.directionalQuadCollectors) {
quadCount += collector.size();
}
if (quadCount == 0) {
return new BuiltSection(section.getKey());
}
var buff = new MemoryBuffer(quadCount*8L);
long ptr = buff.address;
int[] offsets = new int[8];
int coff = 0;
//Ordering is: translucent, double sided quads, directional quads
offsets[0] = coff;
for (long data : this.translucentQuadCollector) {
MemoryUtil.memPutLong(ptr + ((coff++)*8L), data);
}
offsets[1] = coff;
for (long data : this.doubleSidedQuadCollector) {
MemoryUtil.memPutLong(ptr + ((coff++)*8L), data);
}
for (int face = 0; face < 6; face++) {
offsets[face+2] = coff;
for (long data : this.directionalQuadCollectors[face]) {
MemoryUtil.memPutLong(ptr + ((coff++) * 8L), data);
}
connectedData = null;
}
dirId = Direction.EAST.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int x = 0; x < 32; x++) {
this.mesher.reset();
int aabb = 0;
aabb |= this.minX;
aabb |= this.minY<<5;
aabb |= this.minZ<<10;
aabb |= (this.maxX-this.minX)<<15;
aabb |= (this.maxY-this.minY)<<20;
aabb |= (this.maxZ-this.minZ)<<25;
for (int y = 0; y < 32; y++) {
for (int z = 0; z < 32; z++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) {
continue;
}
long up = -1;
if (x < 31) {
up = data[WorldSection.getIndex(x + 1, y, z)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
if (x == 31 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.acquire(section.lvl, section.x + 1, section.y, section.z);
connectedSection.copyDataTo(this.connectedSectionCache);
connectedData = this.connectedSectionCache;
connectedSection.release();
}
up = connectedData[WorldSection.getIndex(0, y, z)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
this.mesher.put(y, z, (self&~(0xFFL<<56))|(up&(0xFFL<<56)));
}
}
var count = this.mesher.process();
var array = this.mesher.getArray();
for (int i = 0; i < count; i++) {
var quad = array[i];
this.outData.add(this.encoder.encode(this.mesher.getDataFromQuad(quad), 5, x, quad));
}
}
connectedData = null;
}
dirId = Direction.SOUTH.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int z = 0; z < 32; z++) {
this.mesher.reset();
for (int x = 0; x < 32; x++) {
for (int y = 0; y < 32; y++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) {
continue;
}
long up = -1;
if (z < 31) {
up = data[WorldSection.getIndex(x, y, z + 1)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
if (z == 31 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.acquire(section.lvl, section.x, section.y, section.z + 1);
connectedSection.copyDataTo(this.connectedSectionCache);
connectedData = this.connectedSectionCache;
connectedSection.release();
}
up = connectedData[WorldSection.getIndex(x, y, 0)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
this.mesher.put(x, y, (self&~(0xFFL<<56))|(up&(0xFFL<<56)));
}
}
var count = this.mesher.process();
var array = this.mesher.getArray();
for (int i = 0; i < count; i++) {
var quad = array[i];
this.outData.add(this.encoder.encode(this.mesher.getDataFromQuad(quad), 3, z, quad));
}
}
connectedData = null;
}
dirId = Direction.WEST.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int x = 31; x != -1; x--) {
this.mesher.reset();
for (int y = 0; y < 32; y++) {
for (int z = 0; z < 32; z++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) {
continue;
}
long up = -1;
if (x != 0) {
up = data[WorldSection.getIndex(x - 1, y, z)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
if (x == 0 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.acquire(section.lvl, section.x - 1, section.y, section.z);
connectedSection.copyDataTo(this.connectedSectionCache);
connectedData = this.connectedSectionCache;
connectedSection.release();
}
up = connectedData[WorldSection.getIndex(31, y, z)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
this.mesher.put(y, z, (self&~(0xFFL<<56))|(up&(0xFFL<<56)));
}
}
var count = this.mesher.process();
var array = this.mesher.getArray();
for (int i = 0; i < count; i++) {
var quad = array[i];
this.outData.add(this.encoder.encode(this.mesher.getDataFromQuad(quad), 4, x, quad));
}
}
connectedData = null;
}
dirId = Direction.NORTH.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int z = 31; z != -1; z--) {
this.mesher.reset();
for (int x = 0; x < 32; x++) {
for (int y = 0; y < 32; y++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) {
continue;
}
long up = -1;
if (z != 0) {
up = data[WorldSection.getIndex(x, y, z - 1)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
if (z == 0 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.acquire(section.lvl, section.x, section.y, section.z - 1);
connectedSection.copyDataTo(this.connectedSectionCache);
connectedData = this.connectedSectionCache;
connectedSection.release();
}
up = connectedData[WorldSection.getIndex(x, y, 31)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
this.mesher.put(x, y, (self&~(0xFFL<<56))|(up&(0xFFL<<56)));
}
}
var count = this.mesher.process();
var array = this.mesher.getArray();
for (int i = 0; i < count; i++) {
var quad = array[i];
this.outData.add(this.encoder.encode(this.mesher.getDataFromQuad(quad), 2, z, quad));
}
}
connectedData = null;
}
dirId = Direction.DOWN.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int y = 31; y != -1; y--) {
this.mesher.reset();
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) {
continue;
}
long up = -1;
if (y != 0) {
up = data[WorldSection.getIndex(x, y - 1, z)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
if (y == 0 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.acquire(section.lvl, section.x, section.y - 1, section.z);
connectedSection.copyDataTo(this.connectedSectionCache);
connectedData = this.connectedSectionCache;
connectedSection.release();
}
up = connectedData[WorldSection.getIndex(x, 31, z)];
if (!Mapper.isTranslucent(up)) {
continue;
}
}
this.mesher.put(x, z, (self&~(0xFFL<<56))|(up&(0xFFL<<56)));
}
}
var count = this.mesher.process();
var array = this.mesher.getArray();
for (int i = 0; i < count; i++) {
var quad = array[i];
this.outData.add(this.encoder.encode(this.mesher.getDataFromQuad(quad), 0, y, quad));
}
}
connectedData = null;
}
if (this.outData.isEmpty()) {
return new BuiltSectionGeometry(section.getKey(), null, null);
}
var output = new MemoryBuffer(this.outData.size()*8L);
for (int i = 0; i < this.outData.size(); i++) {
MemoryUtil.memPutLong(output.address + i * 8L, this.outData.getLong(i));
}
this.outData.clear();
return new BuiltSectionGeometry(section.getKey(), output, null);
return new BuiltSection(section.getKey(), aabb, buff, offsets);
}
private void generateMeshForAxis(WorldSection section, int axisId) {
int aX = axisId==2?1:0;
int aY = axisId==0?1:0;
int aZ = axisId==1?1:0;
//Note the way the connectedSectionCache works is that it reuses the section cache because we know we dont need the connectedSection
// when we are on the other direction
boolean obtainedOppositeSection0 = false;
boolean obtainedOppositeSection31 = false;
for (int primary = 0; primary < 32; primary++) {
this.negativeMesher.reset();
this.positiveMesher.reset();
for (int a = 0; a < 32; a++) {
for (int b = 0; b < 32; b++) {
int x = axisId==2?primary:a;
int y = axisId==0?primary:(axisId==1?b:a);
int z = axisId==1?primary:b;
long self = this.sectionCache[WorldSection.getIndex(x,y,z)];
if (Mapper.isAir(self)) continue;
int selfBlockId = Mapper.getBlockId(self);
long selfMetadata = this.modelMan.getModelMetadata(selfBlockId);
boolean putFace = false;
//Branch into 2 paths, the + direction and -direction, doing it at once makes it much faster as it halves the number of loops
if (ModelManager.faceExists(selfMetadata, axisId<<1)) {//- direction
long facingState = Mapper.AIR;
//Need to access the other connecting section
if (primary == 0) {
if (!obtainedOppositeSection0) {
var connectedSection = this.world.acquire(section.lvl, section.x - aX, section.y - aY, section.z - aZ);
connectedSection.copyDataTo(this.connectedSectionCache);
connectedSection.release();
obtainedOppositeSection0 = true;
}
facingState = this.connectedSectionCache[WorldSection.getIndex(x*(1-aX)+(31*aX), y*(1-aY)+(31*aY), z*(1-aZ)+(31*aZ))];
} else {
facingState = this.sectionCache[WorldSection.getIndex(x-aX, y-aY, z-aZ)];
}
putFace |= this.putFaceIfCan(this.negativeMesher, (axisId<<1), (axisId<<1)|1, self, selfMetadata, selfBlockId, facingState, a, b);
}
if (ModelManager.faceExists(selfMetadata, axisId<<1)) {//+ direction
long facingState = Mapper.AIR;
//Need to access the other connecting section
if (primary == 31) {
if (!obtainedOppositeSection31) {
var connectedSection = this.world.acquire(section.lvl, section.x + aX, section.y + aY, section.z + aZ);
connectedSection.copyDataTo(this.connectedSectionCache);
connectedSection.release();
obtainedOppositeSection31 = true;
}
facingState = this.connectedSectionCache[WorldSection.getIndex(x*(1-aX), y*(1-aY), z*(1-aZ))];
} else {
facingState = this.sectionCache[WorldSection.getIndex(x+aX, y+aY, z+aZ)];
}
putFace |= this.putFaceIfCan(this.positiveMesher, (axisId<<1)|1, (axisId<<1), self, selfMetadata, selfBlockId, facingState, a, b);
}
if (putFace) {
this.minX = Math.min(this.minX, x);
this.minY = Math.min(this.minY, y);
this.minZ = Math.min(this.minZ, z);
this.maxX = Math.max(this.maxX, x);
this.maxY = Math.max(this.maxY, y);
this.maxZ = Math.max(this.maxZ, z);
}
}
}
processMeshedFace(this.negativeMesher, axisId<<1, primary, this.directionalQuadCollectors[(axisId<<1)]);
processMeshedFace(this.positiveMesher, (axisId<<1)|1, primary, this.directionalQuadCollectors[(axisId<<1)|1]);
}
}
//Returns true if a face was placed
private boolean putFaceIfCan(Mesher2D mesher, int face, int opposingFace, long self, long metadata, int selfBlockId, long facingState, int a, int b) {
long facingMetadata = this.modelMan.getModelMetadata(Mapper.getBlockId(facingState));
//If face can be occluded and is occluded from the facing block, then dont render the face
if (ModelManager.faceCanBeOccluded(metadata, face) && ModelManager.faceOccludes(facingMetadata, opposingFace)) {
return false;
}
if (ModelManager.isTranslucent(metadata) && selfBlockId == Mapper.getBlockId(facingState)) {
//If we are facing a block, and are translucent and it is the same block as us, cull the quad
return false;
}
//TODO: if the model has a fluid state, it should compute if a fluid face needs to be injected
// fluid face of type this.world.getMapper().getBlockStateFromId(self).getFluidState() and block type
// this.world.getMapper().getBlockStateFromId(self).getFluidState().getBlockState()
//If we are a fluid and the oposing face is also a fluid need to make a closer check
if (ModelManager.containsFluid(metadata) && ModelManager.containsFluid(facingMetadata)) {
//if (this.world.getMapper().getBlockStateFromId(self).getFluidState() != this.world.getMapper().getBlockStateFromId(facingState).getFluidState()) {
// TODO: need to inject a face here of type fluid, how? i have no god damn idea, probably add an auxilery mesher just for this
//}
//Hackfix
var selfBS = this.world.getMapper().getBlockStateFromId(self);
if (selfBS.getBlock() instanceof FluidBlock && selfBS.getFluidState().getBlockState().getBlock() == this.world.getMapper().getBlockStateFromId(facingState).getFluidState().getBlockState().getBlock()) {
return false;
}
}
int clientModelId = this.modelMan.getModelId(selfBlockId);
long otherFlags = 0;
otherFlags |= ModelManager.isTranslucent(metadata)?1L<<33:0;
otherFlags |= ModelManager.isDoubleSided(metadata)?1L<<34:0;
mesher.put(a, b, ((long)clientModelId) | (((long) Mapper.getLightId(facingState))<<16) | ((((long) Mapper.getBiomeId(self))<<24) * (ModelManager.isBiomeColoured(metadata)?1:0)) | otherFlags);
return true;
}
private void processMeshedFace(Mesher2D mesher, int face, int otherAxis, LongArrayList axisOutputGeometry) {
//TODO: encode translucents and double sided quads to different global buffers
int count = mesher.process();
var array = mesher.getArray();
for (int i = 0; i < count; i++) {
int quad = array[i];
long data = mesher.getDataFromQuad(quad);
long encodedQuad = Integer.toUnsignedLong(QuadEncoder.encodePosition(face, otherAxis, quad)) | ((data&0xFFFF)<<26) | (((data>>16)&0xFF)<<55) | (((data>>24)&0x1FF)<<46);
if ((data&(1L<<33))!=0) {
this.translucentQuadCollector.add(encodedQuad);
} else if ((data&(1L<<34))!=0) {
this.doubleSidedQuadCollector.add(encodedQuad);
} else {
axisOutputGeometry.add(encodedQuad);
}
}
}
}
/*
private static long encodeRaw(int face, int width, int height, int x, int y, int z, int blockId, int biomeId, int lightId) {
return ((long)face) | (((long) width)<<3) | (((long) height)<<7) | (((long) z)<<11) | (((long) y)<<16) | (((long) x)<<21) | (((long) blockId)<<26) | (((long) biomeId)<<46) | (((long) lightId)<<55);
}
*/
/*
//outData.clear();
//var buff = new MemoryBuffer(8*8);
//MemoryUtil.memPutLong(buff.address, encodeRaw(2, 0,1,0,0,0,159,0, 0));//92 515
//MemoryUtil.memPutLong(buff.address+8, encodeRaw(3, 0,1,0,0,0,159,0, 0));//92 515
//MemoryUtil.memPutLong(buff.address+16, encodeRaw(4, 1,2,0,0,0,159,0, 0));//92 515
//MemoryUtil.memPutLong(buff.address+24, encodeRaw(5, 1,2,0,0,0,159,0, 0));//92 515
//MemoryUtil.memPutLong(buff.address+32, encodeRaw(2, 0,1,0,0,1,159,0, 0));//92 515
//MemoryUtil.memPutLong(buff.address+40, encodeRaw(3, 0,1,0,0,1,159,0, 0));//92 515
//MemoryUtil.memPutLong(buff.address+48, encodeRaw(2, 0,1,0,0,2,159,0, 0));//92 515
//MemoryUtil.memPutLong(buff.address+56, encodeRaw(3, 0,1,0,0,2,159,0, 0));//92 515
//int modelId = this.modelMan.getModelId(this.world.getMapper().getIdFromBlockState(Blocks.OAK_FENCE.getDefaultState()));
//int modelId = this.modelMan.getModelId(this.world.getMapper().getIdFromBlockState(Blocks.OAK_FENCE.getDefaultState()));
//outData.add(encodeRaw(0, 0,0,0,0,0, modelId,0, 0));
//outData.add(encodeRaw(1, 0,0,0,0,0, modelId,0, 0));
//outData.add(encodeRaw(2, 0,0,0,0,0, modelId,0, 0));
//outData.add(encodeRaw(3, 0,0,0,0,0, modelId,0, 0));
//outData.add(encodeRaw(4, 0,0,0,0,0, modelId,0, 0));
//outData.add(encodeRaw(5, 0,0,0,0,0, modelId,0, 0));
*/
/*
Mesher2D mesher = new Mesher2D(5,15);
LongArrayList outData = new LongArrayList(1000);
//Up direction
for (int y = 0; y < 32; y++) {
mesher.reset();
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
long self = this.sectionCache[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) continue;
int selfBlockId = Mapper.getBlockId(self);
long metadata = this.modelMan.getModelMetadata(selfBlockId);
//If the model doesnt have a face, then just skip it
if (!ModelManager.faceExists(metadata, 1)) {
continue;
}
long facingState = Mapper.AIR;
//Need to access the other connecting section
if (y == 31) {
} else {
facingState = this.sectionCache[WorldSection.getIndex(x, y+1, z)];
}
long facingMetadata = this.modelMan.getModelMetadata(Mapper.getBlockId(facingState));
//If face can be occluded and is occluded from the facing block, then dont render the face
if (ModelManager.faceCanBeOccluded(metadata, 1) && ModelManager.faceOccludes(facingMetadata, 0)) {
continue;
}
int clientModelId = this.modelMan.getModelId(selfBlockId);
mesher.put(x, z, ((long)clientModelId) | (((long) Mapper.getLightId(facingState))<<16) | (((long) Mapper.getBiomeId(self))<<24));
}
}
//TODO: encode translucents and double sided quads to different global buffers
int count = mesher.process();
var array = mesher.getArray();
for (int i = 0; i < count; i++) {
int quad = array[i];
long data = mesher.getDataFromQuad(quad);
outData.add(Integer.toUnsignedLong(QuadEncoder.encodePosition(1, y, quad)) | ((data&0xFFFF)<<26) | (((data>>16)&0xFF)<<55) | (((data>>24)&0x1FF)<<46));
}
}
//North direction
for (int z = 0; z < 32; z++) {
mesher.reset();
for (int x = 0; x < 32; x++) {
for (int y = 0; y < 32; y++) {
long self = this.sectionCache[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) continue;
int selfBlockId = Mapper.getBlockId(self);
long metadata = this.modelMan.getModelMetadata(selfBlockId);
//If the model doesnt have a face, then just skip it
if (!ModelManager.faceExists(metadata, 2)) {
continue;
}
long facingState = Mapper.AIR;
//Need to access the other connecting section
if (z == 0) {
} else {
facingState = this.sectionCache[WorldSection.getIndex(x, y, z-1)];
}
long facingMetadata = this.modelMan.getModelMetadata(Mapper.getBlockId(facingState));
//If face can be occluded and is occluded from the facing block, then dont render the face
if (ModelManager.faceCanBeOccluded(metadata, 2) && ModelManager.faceOccludes(facingMetadata, 3)) {
continue;
}
int clientModelId = this.modelMan.getModelId(selfBlockId);
mesher.put(x, y, ((long)clientModelId) | (((long) Mapper.getLightId(facingState))<<16) | (((long) Mapper.getBiomeId(self))<<24));
}
}
//TODO: encode translucents and double sided quads to different global buffers
int count = mesher.process();
var array = mesher.getArray();
for (int i = 0; i < count; i++) {
int quad = array[i];
long data = mesher.getDataFromQuad(quad);
outData.add(Integer.toUnsignedLong(QuadEncoder.encodePosition(2, z, quad)) | ((data&0xFFFF)<<26) | (((data>>16)&0xFF)<<55) | (((data>>24)&0x1FF)<<46));
}
}
//South direction
for (int z = 0; z < 32; z++) {
mesher.reset();
for (int x = 0; x < 32; x++) {
for (int y = 0; y < 32; y++) {
long self = this.sectionCache[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) continue;
int selfBlockId = Mapper.getBlockId(self);
long metadata = this.modelMan.getModelMetadata(selfBlockId);
//If the model doesnt have a face, then just skip it
if (!ModelManager.faceExists(metadata, 3)) {
continue;
}
long facingState = Mapper.AIR;
//Need to access the other connecting section
if (z == 31) {
} else {
facingState = this.sectionCache[WorldSection.getIndex(x, y, z+1)];
}
long facingMetadata = this.modelMan.getModelMetadata(Mapper.getBlockId(facingState));
//If face can be occluded and is occluded from the facing block, then dont render the face
if (ModelManager.faceCanBeOccluded(metadata, 3) && ModelManager.faceOccludes(facingMetadata, 2)) {
continue;
}
int clientModelId = this.modelMan.getModelId(selfBlockId);
mesher.put(x, y, ((long)clientModelId) | (((long) Mapper.getLightId(facingState))<<16) | (((long) Mapper.getBiomeId(self))<<24));
}
}
//TODO: encode translucents and double sided quads to different global buffers
int count = mesher.process();
var array = mesher.getArray();
for (int i = 0; i < count; i++) {
int quad = array[i];
long data = mesher.getDataFromQuad(quad);
outData.add(Integer.toUnsignedLong(QuadEncoder.encodePosition(3, z, quad)) | ((data&0xFFFF)<<26) | (((data>>16)&0xFF)<<55) | (((data>>24)&0x1FF)<<46));
}
}
//West direction
for (int x = 0; x < 32; x++) {
mesher.reset();
for (int y = 0; y < 32; y++) {
for (int z = 0; z < 32; z++) {
long self = this.sectionCache[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) continue;
int selfBlockId = Mapper.getBlockId(self);
long metadata = this.modelMan.getModelMetadata(selfBlockId);
//If the model doesnt have a face, then just skip it
if (!ModelManager.faceExists(metadata, 2)) {
continue;
}
long facingState = Mapper.AIR;
//Need to access the other connecting section
if (x == 0) {
} else {
facingState = this.sectionCache[WorldSection.getIndex(x-1, y, z)];
}
long facingMetadata = this.modelMan.getModelMetadata(Mapper.getBlockId(facingState));
//If face can be occluded and is occluded from the facing block, then dont render the face
if (ModelManager.faceCanBeOccluded(metadata, 4) && ModelManager.faceOccludes(facingMetadata, 5)) {
continue;
}
int clientModelId = this.modelMan.getModelId(selfBlockId);
mesher.put(y, z, ((long)clientModelId) | (((long) Mapper.getLightId(facingState))<<16) | (((long) Mapper.getBiomeId(self))<<24));
}
}
//TODO: encode translucents and double sided quads to different global buffers
int count = mesher.process();
var array = mesher.getArray();
for (int i = 0; i < count; i++) {
int quad = array[i];
long data = mesher.getDataFromQuad(quad);
outData.add(Integer.toUnsignedLong(QuadEncoder.encodePosition(4, x, quad)) | ((data&0xFFFF)<<26) | (((data>>16)&0xFF)<<55) | (((data>>24)&0x1FF)<<46));
}
}
//East direction
for (int x = 0; x < 32; x++) {
mesher.reset();
for (int y = 0; y < 32; y++) {
for (int z = 0; z < 32; z++) {
long self = this.sectionCache[WorldSection.getIndex(x, y, z)];
if (Mapper.isAir(self)) continue;
int selfBlockId = Mapper.getBlockId(self);
long metadata = this.modelMan.getModelMetadata(selfBlockId);
//If the model doesnt have a face, then just skip it
if (!ModelManager.faceExists(metadata, 2)) {
continue;
}
long facingState = Mapper.AIR;
//Need to access the other connecting section
if (x == 31) {
} else {
facingState = this.sectionCache[WorldSection.getIndex(x+1, y, z)];
}
long facingMetadata = this.modelMan.getModelMetadata(Mapper.getBlockId(facingState));
//If face can be occluded and is occluded from the facing block, then dont render the face
if (ModelManager.faceCanBeOccluded(metadata, 5) && ModelManager.faceOccludes(facingMetadata, 4)) {
continue;
}
int clientModelId = this.modelMan.getModelId(selfBlockId);
mesher.put(y, z, ((long)clientModelId) | (((long) Mapper.getLightId(facingState))<<16) | (((long) Mapper.getBiomeId(self))<<24));
}
}
//TODO: encode translucents and double sided quads to different global buffers
int count = mesher.process();
var array = mesher.getArray();
for (int i = 0; i < count; i++) {
int quad = array[i];
long data = mesher.getDataFromQuad(quad);
outData.add(Integer.toUnsignedLong(QuadEncoder.encodePosition(5, x, quad)) | ((data&0xFFFF)<<26) | (((data>>16)&0xFF)<<55) | (((data>>24)&0x1FF)<<46));
}
}
*/

View File

@@ -1,6 +1,7 @@
package me.cortex.zenith.client.core.rendering.building;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import me.cortex.zenith.client.core.model.ModelManager;
import me.cortex.zenith.common.world.WorldEngine;
import me.cortex.zenith.common.world.WorldSection;
@@ -22,10 +23,12 @@ public class RenderGenerationService {
private final Semaphore taskCounter = new Semaphore(0);
private final WorldEngine world;
private final Consumer<BuiltSectionGeometry> resultConsumer;
private final ModelManager modelManager;
private final Consumer<BuiltSection> resultConsumer;
public RenderGenerationService(WorldEngine world, int workers, Consumer<BuiltSectionGeometry> consumer) {
public RenderGenerationService(WorldEngine world, ModelManager modelManager, int workers, Consumer<BuiltSection> consumer) {
this.world = world;
this.modelManager = modelManager;
this.resultConsumer = consumer;
this.workers = new Thread[workers];
for (int i = 0; i < workers; i++) {
@@ -36,12 +39,12 @@ public class RenderGenerationService {
}
}
private final ConcurrentHashMap<Long, BuiltSectionGeometry> renderCache = new ConcurrentHashMap<>(1000,0.75f,10);
private final ConcurrentHashMap<Long, BuiltSection> renderCache = new ConcurrentHashMap<>(1000,0.75f,10);
//TODO: add a generated render data cache
private void renderWorker() {
//Thread local instance of the factory
var factory = new RenderDataFactory(this.world);
var factory = new RenderDataFactory(this.world, this.modelManager);
while (this.running) {
this.taskCounter.acquireUninterruptibly();
if (!this.running) break;
@@ -156,6 +159,6 @@ public class RenderGenerationService {
while (!this.taskQueue.isEmpty()) {
this.taskQueue.removeFirst();
}
this.renderCache.values().forEach(BuiltSectionGeometry::free);
this.renderCache.values().forEach(BuiltSection::free);
}
}

View File

@@ -18,7 +18,7 @@ public class BufferArena {
}
this.size = capacity;
this.elementSize = elementSize;
this.buffer = new GlBuffer(capacity, 0);
this.buffer = new GlBuffer(capacity);
this.allocationMap.setLimit(capacity/elementSize);
}

View File

@@ -7,7 +7,7 @@ import net.minecraft.util.math.Box;
import org.joml.Matrix4f;
public class DebugUtil {
private static Matrix4f positionMatrix = new Matrix4f().identity();
public static Matrix4f positionMatrix = new Matrix4f().identity();
public static void setPositionMatrix(MatrixStack stack) {
positionMatrix = new Matrix4f(stack.peek().getPositionMatrix());
}

View File

@@ -0,0 +1,19 @@
package me.cortex.zenith.client.mixin.sodium;
import me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer;
import me.jellysquid.mods.sodium.client.render.chunk.occlusion.OcclusionCuller;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.util.math.MatrixStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = SodiumWorldRenderer.class, remap = false)
public class MixinSodiumWorldRender {
@Inject(method = "drawChunkLayer", at = @At("HEAD"), cancellable = true)
private void cancelRender(RenderLayer renderLayer, MatrixStack matrixStack, double x, double y, double z, CallbackInfo ci) {
//ci.cancel();
}
}

View File

@@ -51,18 +51,20 @@ public class Mapper {
}
public static boolean isTranslucent(long id) {
//Atm hardcode to air
return ((id>>27)&((1<<20)-1)) == 0;
}
public static boolean isAir(long id) {
return ((id>>27)&((1<<20)-1)) == 0;
}
public static boolean shouldRenderFace(int dirId, long self, long other) {
//TODO: fixme make it be with respect to the type itself e.g. water, glass etc
return isTranslucent(other);
public static int getBlockId(long id) {
return (int) ((id>>27)&((1<<20)-1));
}
public static int getBiomeId(long id) {
return (int) ((id>>47)&0x1FF);
}
public static int getLightId(long id) {
return (int) ((id>>56)&0xFF);
}
public void setCallbacks(Consumer<StateEntry> stateCallback, Consumer<BiomeEntry> biomeCallback) {
@@ -183,6 +185,14 @@ public class Mapper {
return this.blockId2stateEntry.get(blockId).state;
}
public int getIdFromBlockState(BlockState state) {
var entry = this.block2stateEntry.get(state);
if (entry == null) {
return -1;
}
return entry.id;
}
//TODO: fixme: synchronize access to this.blockId2stateEntry
public StateEntry[] getStateEntries() {
var set = new ArrayList<>(this.blockId2stateEntry);

View File

@@ -1,6 +1,9 @@
package me.cortex.zenith.common.world.storage;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ShortOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import me.cortex.zenith.common.world.storage.lmdb.LMDBStorageBackend;
import net.minecraft.util.math.random.RandomSeed;
@@ -8,6 +11,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.util.Arrays;
//Segments the section data into multiple dbs
public class FragmentedStorageBackendAdaptor extends StorageBackend {
@@ -57,12 +61,71 @@ public class FragmentedStorageBackendAdaptor extends StorageBackend {
@Override
public void putIdMapping(int id, ByteBuffer data) {
this.backends[0].putIdMapping(id, data);
//Replicate the mappings over all the dbs to mean the chance of recovery in case of corruption is 30x
for (var backend : this.backends) {
backend.putIdMapping(id, data);
}
}
private record EqualingArray(byte[] bytes) {
@Override
public boolean equals(Object obj) {
return Arrays.equals(this.bytes, ((EqualingArray)obj).bytes);
}
@Override
public int hashCode() {
return Arrays.hashCode(this.bytes);
}
}
@Override
public Int2ObjectOpenHashMap<byte[]> getIdMappingsData() {
return this.backends[0].getIdMappingsData();
Object2IntOpenHashMap<Int2ObjectOpenHashMap<EqualingArray>> verification = new Object2IntOpenHashMap<>();
Int2ObjectOpenHashMap<EqualingArray> any = null;
for (var backend : this.backends) {
var mappings = backend.getIdMappingsData();
if (mappings.isEmpty()) {
//TODO: log a warning and attempt to replicate the data the other fragments
continue;
}
var repackaged = new Int2ObjectOpenHashMap<EqualingArray>(mappings.size());
for (var entry : mappings.int2ObjectEntrySet()) {
repackaged.put(entry.getIntKey(), new EqualingArray(entry.getValue()));
}
verification.addTo(repackaged, 1);
any = repackaged;
}
if (any == null) {
return new Int2ObjectOpenHashMap<>();
}
if (verification.size() != 1) {
System.err.println("Error id mapping not matching across all fragments, attempting to recover");
Object2IntMap.Entry<Int2ObjectOpenHashMap<EqualingArray>> maxEntry = null;
for (var entry : verification.object2IntEntrySet()) {
if (maxEntry == null) { maxEntry = entry; }
else {
if (maxEntry.getIntValue() < entry.getIntValue()) {
maxEntry = entry;
}
}
}
var mapping = maxEntry.getKey();
var out = new Int2ObjectOpenHashMap<byte[]>(mapping.size());
for (var entry : mapping.int2ObjectEntrySet()) {
out.put(entry.getIntKey(), entry.getValue().bytes);
}
return out;
} else {
var out = new Int2ObjectOpenHashMap<byte[]>(any.size());
for (var entry : any.int2ObjectEntrySet()) {
out.put(entry.getIntKey(), entry.getValue().bytes);
}
return out;
}
}
@Override

View File

@@ -0,0 +1,8 @@
#version 430
layout(location=0) uniform sampler2D tex;
in vec2 texCoord;
out vec4 colour;
void main() {
colour = texture(tex, texCoord);
}

View File

@@ -0,0 +1,12 @@
#version 430
in vec3 pos;
in vec2 uv;
layout(location=1) uniform mat4 transform;
out vec2 texCoord;
void main() {
gl_Position = transform * vec4(pos, 1.0);
texCoord = uv;
}

View File

@@ -11,19 +11,22 @@ layout(binding = 0, std140) uniform SceneUniform {
uint frameId;
};
struct State {
uint biomeTintMsk;
uint faceColours[6];
};
struct Biome {
uint foliage;
uint water;
struct BlockModel {
uint faceData[6];
uint flagsA;
uint colourTint;
uint _pad[8];
};
struct SectionMeta {
uvec4 header;
uvec4 drawdata;
uint posA;
uint posB;
uint AABB;
uint ptr;
uint cntA;
uint cntB;
uint cntC;
uint cntD;
};
//TODO: see if making the stride 2*4*4 bytes or something cause you get that 16 byte write
@@ -35,6 +38,8 @@ struct DrawCommand {
uint baseInstance;
};
layout(binding = 0) uniform sampler2D blockModelAtlas;
#ifndef Quad
#define Quad ivec2
#endif
@@ -46,23 +51,24 @@ layout(binding = 2, std430) writeonly restrict buffer DrawBuffer {
DrawCommand cmdBuffer[];
};
layout(binding = 3, std430) readonly restrict buffer SectionBuffer {
layout(binding = 3, std430) restrict buffer DrawCommandCountBuffer {
uint opaqueDrawCount;
uint translucentDrawCount;
};
layout(binding = 4, std430) readonly restrict buffer SectionBuffer {
SectionMeta sectionData[];
};
#ifndef VISIBILITY_ACCESS
#define VISIBILITY_ACCESS readonly
#endif
layout(binding = 4, std430) VISIBILITY_ACCESS restrict buffer VisibilityBuffer {
layout(binding = 5, std430) VISIBILITY_ACCESS restrict buffer VisibilityBuffer {
uint visibilityData[];
};
layout(binding = 5, std430) readonly restrict buffer StateBuffer {
State stateData[];
};
layout(binding = 6, std430) readonly restrict buffer BiomeBuffer {
Biome biomeData[];
layout(binding = 6, std430) readonly restrict buffer ModelBuffer {
BlockModel modelData[];
};
layout(binding = 7, std430) readonly restrict buffer LightingBuffer {
@@ -75,3 +81,5 @@ vec4 getLighting(uint index) {
arr = arr & uvec4(0xFF);
return vec4(arr)*vec4(1.0f/255.0f);
}

View File

@@ -0,0 +1,17 @@
//TODO: FIXME: this isnt actually correct cause depending on the face (i think) it could be 1/64 th of a position off
// but im going to assume that since we are dealing with huge render distances, this shouldent matter that much
float extractFaceIndentation(uint faceData) {
return float((faceData>>16)&63)/63;
}
vec4 extractFaceSizes(uint faceData) {
return (vec4(faceData&0xF, (faceData>>4)&0xF, (faceData>>8)&0xF, (faceData>>12)&0xF)/16)+vec4(0,1f/16,0,1f/16);
}
uint faceHasAlphaCuttout(uint faceData) {
return (faceData>>22)&1;
}
uint faceHasAlphaCuttoutOverride(uint faceData) {
return (faceData>>23)&1;
}

View File

@@ -7,6 +7,7 @@ layout(local_size_x = 128, local_size_y = 1, local_size_x = 1) in;
#import <zenith:lod/gl46/bindings.glsl>
#import <zenith:lod/gl46/frustum.glsl>
#import <zenith:lod/gl46/section.glsl>
#line 11
//https://github.com/KhronosGroup/GLSL/blob/master/extensions/ext/GL_EXT_shader_16bit_storage.txt
// adds support for uint8_t which can use for compact visibility buffer
@@ -27,7 +28,16 @@ uint encodeLocalLodPos(uint detail, ivec3 pos) {
}
//TODO: swap to a multidraw indirect counted
//Note: if i want reverse indexing i need to use the index buffer offset to offset
void writeCmd(uint idx, uint encodedPos, uint offset, uint quadCount) {
DrawCommand cmd;
cmd.count = quadCount * 6;
cmd.instanceCount = 1;
cmd.firstIndex = 0;
cmd.baseVertex = int(offset)<<2;
cmd.baseInstance = encodedPos;
cmdBuffer[idx] = cmd;
}
void main() {
if (gl_GlobalInvocationID.x >= sectionCount) {
@@ -55,20 +65,79 @@ void main() {
}
if (shouldRender) {
DrawCommand cmd;
cmd.count = extractQuadCount(meta) * 6;
cmd.instanceCount = 1;
cmd.firstIndex = 0;
cmd.baseVertex = int(extractQuadStart(meta))<<2;
cmd.baseInstance = encodeLocalLodPos(detail, ipos);
cmdBuffer[gl_GlobalInvocationID.x] = cmd;
} else {
DrawCommand cmd;
cmd.count = 0;
cmd.instanceCount = 0;
cmd.firstIndex = 0;
cmd.baseVertex = 0;
cmd.baseInstance = 0;
cmdBuffer[gl_GlobalInvocationID.x] = cmd;
uint encodedPos = encodeLocalLodPos(detail, ipos);
uint ptr = extractQuadStart(meta);
ivec3 relative = ipos-(baseSectionPos>>detail);
//TODO:FIXME: Figure out why these are in such a weird order
uint msk = 0;
msk |= uint(relative.y>-1)<<0;
msk |= uint(relative.y<1 )<<1;
msk |= uint(relative.z>-1)<<2;
msk |= uint(relative.z<1 )<<3;
msk |= uint(relative.x>-1)<<4;
msk |= uint(relative.x<1 )<<5;
uint cmdPtr = atomicAdd(opaqueDrawCount, bitCount(msk)+1);
uint count = 0;
//Translucency
count = meta.cntA&0xFFFF;
if (count != 0) {
uint translucentCommandPtr = atomicAdd(translucentDrawCount, 1) + 400000;//FIXME: dont hardcode this offset
writeCmd(translucentCommandPtr, encodedPos, ptr, count);
}
ptr += count;
//Double sided quads
count = (meta.cntA>>16)&0xFFFF;
writeCmd(cmdPtr++, encodedPos, ptr, count);
ptr += count;
//Down
count = (meta.cntB)&0xFFFF;
if ((msk&(1<<0))!=0) {
writeCmd(cmdPtr++, encodedPos, ptr, count);
}
ptr += count;
//Up
count = (meta.cntB>>16)&0xFFFF;
if ((msk&(1<<1))!=0) {
writeCmd(cmdPtr++, encodedPos, ptr, count);
}
ptr += count;
//North
count = (meta.cntC)&0xFFFF;
if ((msk&(1<<2))!=0) {
writeCmd(cmdPtr++, encodedPos, ptr, count);
}
ptr += count;
//South
count = (meta.cntC>>16)&0xFFFF;
if ((msk&(1<<3))!=0) {
writeCmd(cmdPtr++, encodedPos, ptr, count);
}
ptr += count;
//West
count = (meta.cntD)&0xFFFF;
if ((msk&(1<<4))!=0) {
writeCmd(cmdPtr++, encodedPos, ptr, count);
}
ptr += count;
//East
count = (meta.cntD>>16)&0xFFFF;
if ((msk&(1<<5))!=0) {
writeCmd(cmdPtr++, encodedPos, ptr, count);
}
ptr += count;
}
}

View File

@@ -14,12 +14,16 @@ void main() {
uint detail = extractDetail(section);
ivec3 ipos = extractPosition(section);
ivec3 aabbOffset = extractAABBOffset(section);
ivec3 size = extractAABBSize(section);
//Transform ipos with respect to the vertex corner
ipos += ivec3(gl_VertexID&1, (gl_VertexID>>2)&1, (gl_VertexID>>1)&1);
ivec3 pos = (((ipos<<detail)-baseSectionPos)<<5);
pos += aabbOffset;
pos -= (1<<detail);
pos += (ivec3(gl_VertexID&1, (gl_VertexID>>2)&1, (gl_VertexID>>1)&1)*(size+2))*(1<<detail);
vec3 cornerPos = vec3(((ipos<<detail)-baseSectionPos)<<5);
gl_Position = MVP * vec4(cornerPos,1);
gl_Position = MVP * vec4(vec3(pos),1);
//Write to this id
id = sid;

View File

@@ -17,7 +17,7 @@ uint extractFace(uint64_t quad) {
}
uint extractStateId(uint64_t quad) {
return Eu32(quad, 20, 26);
return Eu32(quad, 16, 26);
}
uint extractBiomeId(uint64_t quad) {

View File

@@ -1,8 +1,22 @@
#version 460 core
layout(location = 0) in flat vec4 colour;
layout(binding = 0) uniform sampler2D blockModelAtlas;
//TODO: need to fix when merged quads have discardAlpha set to false but they span multiple tiles
// however they are not a full block
layout(location = 0) in vec2 uv;
layout(location = 1) in flat vec2 baseUV;
layout(location = 2) in flat vec4 colourTinting;
layout(location = 3) in flat uint discardAlpha;
layout(location = 0) out vec4 outColour;
void main() {
//TODO: randomly discard the fragment with respect to the alpha value
vec2 uv = mod(uv, vec2(1))*(1f/(vec2(3,2)*256f));
vec4 colour = texture(blockModelAtlas, uv + baseUV);
if (discardAlpha == 1 && colour.a == 0.0f) {
discard;
}
outColour = colour * colourTinting;
outColour = colour;
//outColour = vec4(uv + baseUV, 0, 1);
}

View File

@@ -3,8 +3,13 @@
#import <zenith:lod/gl46/quad_format.glsl>
#import <zenith:lod/gl46/bindings.glsl>
#import <zenith:lod/gl46/block_model.glsl>
#line 8
layout(location = 0) out flat vec4 colour;
layout(location = 0) out vec2 uv;
layout(location = 1) out flat vec2 baseUV;
layout(location = 2) out flat vec4 colourTinting;
layout(location = 3) out flat uint discardAlpha;
uint extractLodLevel() {
return uint(gl_BaseInstance)>>29;
@@ -20,85 +25,85 @@ vec4 uint2vec4RGBA(uint colour) {
return vec4((uvec4(colour)>>uvec4(24,16,8,0))&uvec4(0xFF))/255;
}
//Gets the face offset with respect to the face direction (e.g. some will be + some will be -)
float getDepthOffset(uint faceData, uint face) {
float offset = extractFaceIndentation(faceData);
return offset * (1-((int(face)&1)*2));
}
vec2 getFaceSizeOffset(uint faceData, uint corner) {
vec4 faceOffsetsSizes = extractFaceSizes(faceData);
return mix(faceOffsetsSizes.xz, -(1-faceOffsetsSizes.yw), bvec2(((corner>>1)&1)==1, (corner&1)==1));
}
//TODO: add a mechanism so that some quads can ignore backface culling
// this would help alot with stuff like crops as they would look kinda weird i think,
// same with flowers etc
void main() {
int cornerIdx = gl_VertexID&3;
Quad quad = quadData[uint(gl_VertexID)>>2];
vec3 innerPos = extractPos(quad);
uint face = extractFace(quad);
uint modelId = extractStateId(quad);
BlockModel model = modelData[modelId];
uint faceData = model.faceData[face];
//Change the ordering due to backface culling
//NOTE: when rendering, backface culling is disabled as we simply dispatch calls for each face
// this has the advantage of having "unassigned" geometry, that is geometry where the backface isnt culled
//if (face == 0 || (face>>1 != 0 && (face&1)==1)) {
// cornerIdx ^= 1;
//}
uint lodLevel = extractLodLevel();
ivec3 lodCorner = ((extractRelativeLodPos()<<lodLevel) - (baseSectionPos&(ivec3((1<<lodLevel)-1))))<<5;
vec3 corner = innerPos * (1<<lodLevel) + lodCorner;
//TODO: see if backface culling is even needed, since everything (should) be back culled already
//Flip the quad rotation by its face (backface culling)
if ((face&1) != 0) {
cornerIdx ^= 1;
vec2 faceOffset = getFaceSizeOffset(faceData, cornerIdx);
ivec2 quadSize = extractSize(quad);
vec2 respectiveQuadSize = vec2(quadSize * ivec2((cornerIdx>>1)&1, cornerIdx&1));
vec2 size = (respectiveQuadSize + faceOffset) * (1<<lodLevel);
vec3 offset = vec3(size, (float(face&1) + getDepthOffset(faceData, face)) * (1<<lodLevel));
if ((face>>1) == 0) {//Up/down
offset = offset.xzy;
}
if ((face>>1) == 0) {
cornerIdx ^= 1;
//Not needed, here for readability
//if ((face>>1) == 1) {//north/south
// offset = offset.xyz;
//}
if ((face>>1) == 2) {//west/east
offset = offset.zxy;
}
ivec2 size = extractSize(quad) * ivec2((cornerIdx>>1)&1, cornerIdx&1) * (1<<lodLevel);
gl_Position = MVP * vec4(corner + offset,1);
vec3 pos = corner;
//NOTE: can just make instead of face, make it axis (can also make it 2 bit instead of 3 bit then)
// since the only reason face is needed is to ensure backface culling orientation thing
uint axis = face>>1;
if (axis == 0) {
pos.xz += size;
pos.y += (face&1)<<lodLevel;
} else if (axis == 1) {
pos.xy += size;
pos.z += (face&1)<<lodLevel;
} else {
pos.yz += size;
pos.x += (face&1)<<lodLevel;
}
//Compute the uv coordinates
vec2 modelUV = vec2(modelId&0xFF, (modelId>>8)&0xFF)*(1f/(256f));
//TODO: make the face orientated by 2x3 so that division is not a integer div and modulo isnt needed
// as these are very slow ops
baseUV = modelUV + (vec2(face%3, face/3) * (1f/(vec2(3,2)*256f)));
uv = respectiveQuadSize + faceOffset;//Add in the face offset for 0,0 uv
gl_Position = MVP * vec4(pos,1);
discardAlpha = faceHasAlphaCuttout(faceData);
uint stateId = extractStateId(quad);
uint biomeId = extractBiomeId(quad);
State stateInfo = stateData[stateId];
colour = uint2vec4RGBA(stateInfo.faceColours[face]);
//We need to have a conditional override based on if the model size is < a full face + quadSize > 1
discardAlpha |= uint(any(greaterThan(quadSize, ivec2(1)))) & faceHasAlphaCuttoutOverride(faceData);
colour *= getLighting(extractLightId(quad));
//Compute lighting
colourTinting = getLighting(extractLightId(quad));
if (((stateInfo.biomeTintMsk>>face)&1) == 1) {
vec4 biomeColour = uint2vec4RGBA(biomeData[biomeId].foliage);
colour *= biomeColour;
}
//Apply water tint
if (((stateInfo.biomeTintMsk>>6)&1) == 1) {
colour *= vec4(0.247, 0.463, 0.894, 1);
}
//Apply model colour tinting
colourTinting *= uint2vec4RGBA(model.colourTint).yzwx;
//Apply face tint
if (face == 0) {
colour.xyz *= vec3(0.75, 0.75, 0.75);
colourTinting.xyz *= vec3(0.75, 0.75, 0.75);
} else if (face != 1) {
colour.xyz *= vec3((float(face-2)/4)*0.6 + 0.4);
colourTinting.xyz *= vec3((float(face-2)/4)*0.6 + 0.4);
}
}
//gl_Position = MVP * vec4(vec3(((cornerIdx)&1)+10,10,((cornerIdx>>1)&1)+10),1);
//uint i = uint(quad>>32);
//uint i = uint(gl_BaseInstance);
//i ^= i>>16;
//i *= 124128573;
//i ^= i>>16;
//i *= 4211346123;
//i ^= i>>16;
//i *= 462312435;
//i ^= i>>16;
//i *= 542354341;
//i ^= i>>16;
//uint i = uint(lodLevel+12)*215387625;
//colour *= vec4(vec3(float((uint(i)>>2)&7)/7,float((uint(i)>>5)&7)/7,float((uint(i)>>8)&7)/7)*0.7+0.3,1);
//colour = vec4(vec3(float((uint(i)>>2)&7)/7,float((uint(i)>>5)&7)/7,float((uint(i)>>8)&7)/7),1);

View File

@@ -1,21 +1,25 @@
uint extractDetail(SectionMeta section) {
return section.header.x>>28;
return section.posA>>28;
}
ivec3 extractPosition(SectionMeta section) {
int y = ((int(section.header.x)<<4)>>24);
int x = (int(section.header.y)<<4)>>8;
int z = int((section.header.x&((1<<20)-1))<<4);
z |= int(section.header.y>>28);
int y = ((int(section.posA)<<4)>>24);
int x = (int(section.posB)<<4)>>8;
int z = int((section.posA&((1<<20)-1))<<4);
z |= int(section.posB>>28);
z <<= 8;
z >>= 8;
return ivec3(x,y,z);
}
uint extractQuadStart(SectionMeta meta) {
return meta.header.z;
return meta.ptr;
}
uint extractQuadCount(SectionMeta meta) {
return meta.header.w;
ivec3 extractAABBOffset(SectionMeta meta) {
return (ivec3(meta.AABB)>>ivec3(0,5,10))&31;
}
ivec3 extractAABBSize(SectionMeta meta) {
return ((ivec3(meta.AABB)>>ivec3(15,20,25))&31)+1;//The size is + 1 cause its always at least 1x1x1
}

View File

@@ -3,3 +3,4 @@ accessWidener v1 named
accessible field net/minecraft/client/texture/SpriteContents image Lnet/minecraft/client/texture/NativeImage;
accessible field net/minecraft/client/render/Frustum frustumIntersection Lorg/joml/FrustumIntersection;
accessible field net/minecraft/client/render/LightmapTextureManager texture Lnet/minecraft/client/texture/NativeImageBackedTexture;
accessible field net/minecraft/client/color/block/BlockColors providers Lnet/minecraft/util/collection/IdList;

View File

@@ -15,6 +15,7 @@
"defaultRequire": 1
},
"mixins": [
"sodium.MixinOcclusionCuller"
"sodium.MixinOcclusionCuller",
"sodium.MixinSodiumWorldRender"
]
}