mirror of
https://github.com/GTNewHorizons/GTNHLib.git
synced 2026-11-08 12:34:52 +00:00
Updates to JSON model API (#100)
This commit is contained in:
1 parent
6d428aea87
commit
6529f347ec
102 files changed
+3991
-3575
No files matched your search
@@ -20,6 +20,8 @@ dependencies {
|
||||
compileOnly("com.github.GTNewHorizons:lwjgl3ify:2.1.17") { transitive = false }
|
||||
compileOnly("com.gtnewhorizons.retrofuturabootstrap:RetroFuturaBootstrap:1.0.10") { transitive = false }
|
||||
|
||||
// Annotation things
|
||||
compileOnly("com.github.GTNewHorizons:Angelica:1.0.0-beta62:dev") { transitive = false }
|
||||
compileOnly('org.jetbrains:annotations:26.0.1')
|
||||
compileOnly("org.projectlombok:lombok:1.18.36") {transitive = false }
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.36")
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Model API
|
||||
|
||||
In general, it is designed to mimic the latest version.
|
||||
|
||||
### Intentional deviations
|
||||
Texture references are assumed to be in `assets/<domain>/blocks` by
|
||||
default, to match 7.10 conventions. However, textures specified by
|
||||
`domain:block/whatever` are remapped to `domain:whatever`, to support
|
||||
importing models from modern.
|
||||
|
||||
Resource packs can't replace any block. It has to return
|
||||
`ModelISBRH.JSON_ISBRH_ID` as the render type. This is a fairly
|
||||
trivial mixin, but the requirement is still there to avoid
|
||||
spurious lookups on the vast majority of blocks that (currently)
|
||||
don't have models. This may change in the future.
|
||||
|
||||
### TODO
|
||||
- Autoload item textures too.
|
||||
- Add smooth shading to ModelISBRH.
|
||||
- Add face culling to models.
|
||||
- Implement UV locking.
|
||||
- Integrate with a proper BlockState API.
|
||||
- BlockItem rendering.
|
||||
@@ -1,14 +1,17 @@
|
||||
package com.gtnewhorizon.gtnhlib;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.ModelLoader.shouldLoadModels;
|
||||
import static com.gtnewhorizon.gtnhlib.GTNHLib.MODID;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.resources.SimpleReloadableResourceManager;
|
||||
import net.minecraft.util.ChatComponentText;
|
||||
import net.minecraft.util.EnumChatFormatting;
|
||||
import net.minecraft.util.IChatComponent;
|
||||
import net.minecraftforge.client.ClientCommandHandler;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.ModelLoader;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.ModelISBRH;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry;
|
||||
import com.gtnewhorizon.gtnhlib.client.tooltip.LoreHandler;
|
||||
import com.gtnewhorizon.gtnhlib.commands.ItemInHandCommand;
|
||||
import com.gtnewhorizon.gtnhlib.compat.FalseTweaks;
|
||||
@@ -17,10 +20,11 @@ import com.gtnewhorizon.gtnhlib.compat.NotEnoughItemsVersionChecker;
|
||||
import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber;
|
||||
import com.gtnewhorizon.gtnhlib.util.AboveHotbarHUD;
|
||||
|
||||
import cpw.mods.fml.client.registry.RenderingRegistry;
|
||||
import cpw.mods.fml.common.FMLCommonHandler;
|
||||
import cpw.mods.fml.common.event.*;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.common.gameevent.TickEvent;
|
||||
import cpw.mods.fml.common.event.FMLInitializationEvent;
|
||||
import cpw.mods.fml.common.event.FMLPostInitializationEvent;
|
||||
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -34,6 +38,17 @@ public class ClientProxy extends CommonProxy {
|
||||
public static int currentServerViewDistance = 12;
|
||||
private final Minecraft mc = Minecraft.getMinecraft();
|
||||
|
||||
@Override
|
||||
public void preInit(FMLPreInitializationEvent event) {
|
||||
super.preInit(event);
|
||||
|
||||
RenderingRegistry.registerBlockHandler(new ModelISBRH());
|
||||
|
||||
// External model loader handlers. For the low low price of calling this method (and having your jar scanned),
|
||||
// you too can automatically load textures for your models!
|
||||
ModelRegistry.registerModid(MODID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FMLInitializationEvent event) {
|
||||
super.init(event);
|
||||
@@ -54,12 +69,12 @@ public class ClientProxy extends CommonProxy {
|
||||
FMLCommonHandler.instance().bus().register(new NotEnoughItemsVersionChecker());
|
||||
}
|
||||
|
||||
if (shouldLoadModels()) {
|
||||
Minecraft.getMinecraft().refreshResources();
|
||||
ModelLoader.loadModels();
|
||||
}
|
||||
|
||||
LoreHandler.postInit();
|
||||
|
||||
// Internal model loader handlers
|
||||
final var resourceManager = ((SimpleReloadableResourceManager) Minecraft.getMinecraft().getResourceManager());
|
||||
resourceManager.registerReloadListener(new ModelRegistry.ReloadListener());
|
||||
MinecraftForge.EVENT_BUS.register(new ModelRegistry.EventHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,12 +137,4 @@ public class ClientProxy extends CommonProxy {
|
||||
public void printMessageAboveHotbar(String message, int displayDuration, boolean drawShadow, boolean shouldFade) {
|
||||
AboveHotbarHUD.renderTextAboveHotbar(message, displayDuration, drawShadow, shouldFade);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onTick(TickEvent.ClientTickEvent event) {
|
||||
if (!modelsBaked) {
|
||||
ModelLoader.bakeModels();
|
||||
modelsBaked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.util.IChatComponent;
|
||||
import net.minecraftforge.common.util.FakePlayer;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.config.ConfigException;
|
||||
import com.gtnewhorizon.gtnhlib.block.BlockTest;
|
||||
import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
|
||||
import com.gtnewhorizon.gtnhlib.eventbus.AutoEventBus;
|
||||
import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber;
|
||||
@@ -27,6 +27,7 @@ import cpw.mods.fml.common.event.FMLServerStoppedEvent;
|
||||
import cpw.mods.fml.common.event.FMLServerStoppingEvent;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.common.gameevent.PlayerEvent;
|
||||
import cpw.mods.fml.common.registry.GameRegistry;
|
||||
|
||||
@EventBusSubscriber
|
||||
public class CommonProxy {
|
||||
@@ -38,10 +39,9 @@ public class CommonProxy {
|
||||
public void preInit(FMLPreInitializationEvent event) {
|
||||
AutoEventBus.executePhase(Phase.PRE);
|
||||
GTNHLib.info("GTNHLib version " + Tags.VERSION + " loaded.");
|
||||
try {
|
||||
ConfigurationManager.registerConfig(GTNHLibConfig.class);
|
||||
} catch (ConfigException e) {
|
||||
GTNHLib.LOG.error("Failed to register GTNHLib config!", e);
|
||||
|
||||
if (GTNHLibConfig.enableTestBlocks) {
|
||||
GameRegistry.registerBlock(new BlockTest(), "model_test");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,4 +9,20 @@ public class GTNHLibConfig {
|
||||
@Config.DefaultBoolean(false)
|
||||
public static boolean ignoreNEIVersion;
|
||||
|
||||
@Config.Comment("Larger values take more RAM, but require less model rebuilding (may reduce lag spikes). Unless you are very short on RAM, reducing this is not advised.")
|
||||
@Config.DefaultInt(1000)
|
||||
@Config.RangeInt(min = 1, max = 1_000_000)
|
||||
@Config.RequiresMcRestart
|
||||
public static int modelCacheSize;
|
||||
|
||||
@Config.Comment("Automatically load model textures based on model files. Disabling may cause models to lose their textures.")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean autoTextureLoading;
|
||||
|
||||
@Config.Comment("If you're not a dev, you don't need this")
|
||||
@Config.DefaultBoolean(false)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean enableTestBlocks;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.gtnewhorizon.gtnhlib.block;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.ModelISBRH.JSON_ISBRH_ID;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.material.Material;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class BlockTest extends Block {
|
||||
|
||||
public BlockTest() {
|
||||
super(Material.wood);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpaqueCube() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onBlockPlaced(@NotNull World worldIn, int x, int y, int z, int side, float subX, float subY, float subZ,
|
||||
int meta) {
|
||||
|
||||
// Face NORTH if placed up or down
|
||||
final var s = ForgeDirection.getOrientation(side);
|
||||
if (s == ForgeDirection.UP || s == ForgeDirection.DOWN) return 2;
|
||||
|
||||
// Face the placed side
|
||||
return side - 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRenderType() {
|
||||
return JSON_ISBRH_ID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Matrix4fc;
|
||||
|
||||
public interface BakeData {
|
||||
|
||||
BakeData IDENTITY = new BakeData() {
|
||||
|
||||
private final Matrix4f I = new Matrix4f();
|
||||
|
||||
@Override
|
||||
public Matrix4fc getAffineMatrix() {
|
||||
return I;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @return The affine matrix of the transformation. It may be reused - copy before mutating it!
|
||||
*/
|
||||
Matrix4fc getAffineMatrix();
|
||||
|
||||
/**
|
||||
* @return True if textures shouldn't be rotated with the rest of the model; false otherwise.
|
||||
*/
|
||||
default boolean lockUV() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.util.IIcon;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.IBlockPos;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadBuilder;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadProvider;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
|
||||
|
||||
public class DynamicCubeModel implements QuadProvider {
|
||||
|
||||
/*
|
||||
* By the power of ~~theft~~ inspiration, this class is wayyy simpler. It's also a basic example of dynamic model
|
||||
* building.
|
||||
*/
|
||||
|
||||
public static final ThreadLocal<DynamicCubeModel> INSTANCE = ThreadLocal.withInitial(DynamicCubeModel::new);
|
||||
private final NdQuadBuilder builder = new NdQuadBuilder();
|
||||
private static final List<QuadView> EMPTY = ObjectImmutableList.of();
|
||||
private final List<QuadView> ONE = new ObjectArrayList<>(1);
|
||||
|
||||
public DynamicCubeModel() {
|
||||
|
||||
this.ONE.add(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDynamic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuadView> getQuads(IBlockAccess world, IBlockPos pos, Block block, int meta, ForgeDirection dir,
|
||||
Random random, int color, Supplier<QuadView> quadPool) {
|
||||
|
||||
if (dir == ForgeDirection.UNKNOWN) return EMPTY;
|
||||
|
||||
this.builder.square(dir, 0, 0, 1, 1, 0);
|
||||
final IIcon tex = block.getIcon(dir.ordinal(), meta);
|
||||
this.builder.spriteBake(tex, QuadBuilder.BAKE_LOCK_UV);
|
||||
|
||||
this.builder.setColors(color);
|
||||
|
||||
ONE.set(0, this.builder.build(quadPool.get()));
|
||||
return ONE;
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Axis;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Quad;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.util.MathUtil;
|
||||
|
||||
/**
|
||||
* Static routines of general utility for renderer implementations. Renderers are not required to use these helpers, but
|
||||
* they were designed to be usable without the default renderer.
|
||||
*/
|
||||
public abstract class GeometryHelper {
|
||||
|
||||
/**
|
||||
* Returns true if quad is parallel to the given face. Does not validate quad winding order. Expects convex quads
|
||||
* with all points co-planar.
|
||||
*/
|
||||
public static boolean isQuadParallelToFace(ForgeDirection face, NdQuadBuilder quad) {
|
||||
int i = Axis.fromDirection(face).ordinal();
|
||||
final float val = quad.posByIndex(0, i);
|
||||
return MathUtil.fuzzy_eq(val, quad.posByIndex(1, i)) && MathUtil.fuzzy_eq(val, quad.posByIndex(2, i))
|
||||
&& MathUtil.fuzzy_eq(val, quad.posByIndex(3, i));
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the face to which the quad is most closely aligned. This mimics the value that
|
||||
* {@link Quad#getLightFace()} returns, and is used in the vanilla renderer for all diffuse lighting.
|
||||
*
|
||||
* <p>
|
||||
* Derived from the quad face normal and expects convex quads with all points co-planar.
|
||||
*/
|
||||
public static ForgeDirection lightFace(NdQuadBuilder quad) {
|
||||
final Vector3f normal = quad.faceNormal;
|
||||
return coerceVector(normal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the direction closest to the given vector.
|
||||
*/
|
||||
private static ForgeDirection coerceVector(Vector3f v) {
|
||||
return switch (GeometryHelper.longestAxis(v)) {
|
||||
case X -> v.x() > 0 ? ForgeDirection.EAST : ForgeDirection.WEST;
|
||||
case Y -> v.y() > 0 ? ForgeDirection.UP : ForgeDirection.DOWN;
|
||||
case Z -> v.z() > 0 ? ForgeDirection.SOUTH : ForgeDirection.NORTH;
|
||||
default -> ForgeDirection.UP; // handle WTF case
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #longestAxis(float, float, float)
|
||||
*/
|
||||
public static Axis longestAxis(Vector3f vec) {
|
||||
return longestAxis(vec.x(), vec.y(), vec.z());
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the largest (max absolute magnitude) component (X, Y, Z) in the given vector.
|
||||
*/
|
||||
public static Axis longestAxis(float normalX, float normalY, float normalZ) {
|
||||
Axis result = Axis.Y;
|
||||
float longest = Math.abs(normalY);
|
||||
float a = Math.abs(normalX);
|
||||
|
||||
if (a > longest) {
|
||||
result = Axis.X;
|
||||
longest = a;
|
||||
}
|
||||
|
||||
return Math.abs(normalZ) > longest ? Axis.Z : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the given direction by the matrix, and returns the closest direction to the output.
|
||||
*/
|
||||
public static ForgeDirection rotate(ForgeDirection d, Matrix4f rotMat) {
|
||||
return rotate(d, rotMat, new Vector3f());
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link #rotate(ForgeDirection, Matrix4f)}. This overload allows you to pass a scratch vector instead.
|
||||
*/
|
||||
public static ForgeDirection rotate(ForgeDirection d, Matrix4f rotMat, Vector3f v) {
|
||||
return coerceVector(v.set(d.offsetX, d.offsetY, d.offsetZ).mulPosition(rotMat));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus.Internal;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Matrix4fc;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ResourceLoc;
|
||||
|
||||
/// Mirrors Minecraft's format for specifying models.
|
||||
@Internal
|
||||
public final class JSONVariant implements BakeData {
|
||||
|
||||
/// @param weight Used when selecting models. Irrelevant when only one variant is allowed, but must always be
|
||||
/// greater than zero.
|
||||
public static Weighted<JSONVariant> weightedVariant(ResourceLoc.ModelLoc model, int x, int y, boolean uvLock,
|
||||
int weight) {
|
||||
final var v = new JSONVariant(model, x, y, uvLock);
|
||||
return new Weighted<>(v, weight);
|
||||
}
|
||||
|
||||
private static final float DEG2RAD = (float) (Math.PI / 180);
|
||||
private final ResourceLoc.ModelLoc model;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final boolean uvLock;
|
||||
|
||||
/// @param model The model file this variant refers to
|
||||
/// @param x X rotation
|
||||
/// @param y Y rotation
|
||||
/// @param uvLock If true, textures aren't rotated with the block
|
||||
public JSONVariant(ResourceLoc.ModelLoc model, int x, int y, boolean uvLock) {
|
||||
this.model = model;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.uvLock = uvLock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Matrix4fc getAffineMatrix() {
|
||||
return new Matrix4f().translation(-.5f, -.5f, -.5f).rotateLocalX(x * DEG2RAD).rotateLocalY(y * DEG2RAD)
|
||||
.translateLocal(.5f, .5f, .5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean lockUV() {
|
||||
return uvLock;
|
||||
}
|
||||
|
||||
public ResourceLoc.ModelLoc model() {
|
||||
return model;
|
||||
}
|
||||
|
||||
public int x() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public int y() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public boolean uvLock() {
|
||||
return uvLock;
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
This package is licensed under the LGPLv3 for the general public, as noted in the main license file.
|
||||
|
||||
The original files were licensed under the Apache License, Version 2.0.
|
||||
Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
|
||||
Fabric (and any derivatives) may use this package under either the LGPLv3 or the Apache License, Version 2.0, reproduced below.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
public class Material {
|
||||
|
||||
/**
|
||||
* The following masks control whether the property is enabled.
|
||||
*/
|
||||
|
||||
// If true, renders face as fullbright
|
||||
private static final int EMISSIVE_MASK = 1;
|
||||
private static final int COLOR_MASK = 1 << 1;
|
||||
// If true, uses diffuse shading/shadows
|
||||
private static final int DIFFUSE_MASK = 1 << 2;
|
||||
private static final int AO_MASK = 1 << 3;
|
||||
|
||||
// By default, quads use diffuse shading and AO
|
||||
private static final int DEFAULTS = DIFFUSE_MASK | AO_MASK;
|
||||
|
||||
private int flags = DEFAULTS;
|
||||
|
||||
public void reset() {
|
||||
this.flags = DEFAULTS;
|
||||
}
|
||||
|
||||
private void setMask(int mask, boolean val) {
|
||||
if (val) this.flags |= mask;
|
||||
else this.flags &= ~mask;
|
||||
}
|
||||
|
||||
private boolean getMask(int mask) {
|
||||
return (this.flags & mask) == mask;
|
||||
}
|
||||
|
||||
public void setAO(boolean val) {
|
||||
this.setMask(AO_MASK, val);
|
||||
}
|
||||
|
||||
public boolean getAO() {
|
||||
return getMask(AO_MASK);
|
||||
}
|
||||
|
||||
public void setDiffuse(boolean val) {
|
||||
this.setMask(DIFFUSE_MASK, val);
|
||||
}
|
||||
|
||||
public boolean getDiffuse() {
|
||||
return getMask(DIFFUSE_MASK);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util.NormI8.unpackX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util.NormI8.unpackY;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util.NormI8.unpackZ;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.DIRECTIONS;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.client.renderer.RenderBlocks;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.util.IIcon;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.baked.BakedModel;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.BlockState;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
|
||||
import com.gtnewhorizons.angelica.api.ThreadSafeISBRH;
|
||||
|
||||
import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler;
|
||||
import cpw.mods.fml.client.registry.RenderingRegistry;
|
||||
|
||||
@ThreadSafeISBRH(perThread = true)
|
||||
public class ModelISBRH implements ISimpleBlockRenderingHandler {
|
||||
|
||||
/**
|
||||
* Any blocks using a JSON model should return this for {@link Block#getRenderType()}.
|
||||
*/
|
||||
public static final int JSON_ISBRH_ID = RenderingRegistry.getNextAvailableRenderId();
|
||||
|
||||
private final Random RAND = new Random();
|
||||
|
||||
public ModelISBRH() {}
|
||||
|
||||
/// Override this if you want programmatic model selection
|
||||
@SuppressWarnings("unused")
|
||||
protected BakedModel getModel(IBlockAccess world, Block block, int meta, int x, int y, int z) {
|
||||
return ModelRegistry.getBakedModel(new BlockState(block, meta));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderInventoryBlock(Block block, int metadata, int modelId, RenderBlocks renderer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean renderWorldBlock(IBlockAccess world, int x, int y, int z, Block block, int modelId,
|
||||
RenderBlocks renderer) {
|
||||
final Random random = world instanceof World worldIn ? worldIn.rand : RAND;
|
||||
final Tessellator tesselator = TessellatorManager.get();
|
||||
|
||||
// Get the model!
|
||||
final int meta = world.getBlockMetadata(x, y, z);
|
||||
final var model = getModel(world, block, meta, x, y, z);
|
||||
|
||||
int color = model.getColor(world, x, y, z, block, meta, random);
|
||||
|
||||
var rendered = false;
|
||||
for (var dir : DIRECTIONS) {
|
||||
// TODO: face culling
|
||||
|
||||
final var quads = model.getQuads(world, x, y, z, block, meta, dir, random, color, null);
|
||||
if (quads.isEmpty()) continue;
|
||||
|
||||
// iterates over the quads and dumps em into the tesselator, nothing special
|
||||
rendered = true;
|
||||
for (final var quad : quads) {
|
||||
|
||||
if (quad.getColorIndex() != -1 && color == -1) {
|
||||
color = block.colorMultiplier(world, x, y, z);
|
||||
}
|
||||
|
||||
final float r = (color & 255) / 255f;
|
||||
final float g = (color >> 8 & 255) / 255f;
|
||||
final float b = (color >> 16 & 255) / 255f;
|
||||
|
||||
final int lm = getLightMap(block, quad, dir, world, x, y, z, renderer);
|
||||
tesselator.setBrightness(lm);
|
||||
|
||||
final float shade = diffuseLight(quad.getComputedFaceNormal());
|
||||
tesselator.setColorOpaque_F(r * shade, g * shade, b * shade);
|
||||
renderQuad(quad, x, y, z, tesselator, null);
|
||||
}
|
||||
}
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
protected void renderQuad(ModelQuadView quad, float x, float y, float z, Tessellator tessellator,
|
||||
@Nullable IIcon overrideIcon) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
tessellator.addVertexWithUV(
|
||||
quad.getX(i) + x,
|
||||
quad.getY(i) + y,
|
||||
quad.getZ(i) + z,
|
||||
quad.getTexU(i),
|
||||
quad.getTexV(i));
|
||||
}
|
||||
}
|
||||
|
||||
private int getLightMap(Block block, ModelQuadView quad, ModelQuadFacing dir, IBlockAccess world, int x, int y,
|
||||
int z, RenderBlocks rb) {
|
||||
// If the face is aligned or external, pick light outside
|
||||
final float avgPos = getAveragePos(quad, dir);
|
||||
switch (dir) {
|
||||
case POS_X, POS_Y, POS_Z -> {
|
||||
if (avgPos >= 1.0) {
|
||||
final int lx = x + dir.getStepX();
|
||||
final int ly = y + dir.getStepY();
|
||||
final int lz = z + dir.getStepZ();
|
||||
return block.getMixedBrightnessForBlock(world, lx, ly, lz);
|
||||
}
|
||||
}
|
||||
case NEG_X, NEG_Y, NEG_Z -> {
|
||||
if (avgPos <= 0.0) {
|
||||
final int lx = x + dir.getStepX();
|
||||
final int ly = y + dir.getStepY();
|
||||
final int lz = z + dir.getStepZ();
|
||||
return block.getMixedBrightnessForBlock(world, lx, ly, lz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The face is inset to some degree, pick self light (if transparent)
|
||||
if (block.getLightOpacity(world, x, y, z) != 0) {
|
||||
return block.getMixedBrightnessForBlock(world, x, y, z);
|
||||
}
|
||||
|
||||
// ...or greatest among neighbors otherwise
|
||||
int lm = block.getMixedBrightnessForBlock(world, x, y, z);;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
final var neighbor = DIRECTIONS[i];
|
||||
final int lx = x + neighbor.getStepX();
|
||||
final int ly = y + neighbor.getStepY();
|
||||
final int lz = z + neighbor.getStepZ();
|
||||
lm = max(lm, block.getMixedBrightnessForBlock(world, lx, ly, lz));
|
||||
}
|
||||
|
||||
return lm;
|
||||
}
|
||||
|
||||
private float getAveragePos(ModelQuadView quad, ModelQuadFacing dir) {
|
||||
float avg = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
avg += switch (dir.getAxis()) {
|
||||
case X -> quad.getX(i);
|
||||
case Y -> quad.getY(i);
|
||||
case Z -> quad.getZ(i);
|
||||
};
|
||||
}
|
||||
return avg / 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRender3DInInventory(int modelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRenderId() {
|
||||
return JSON_ISBRH_ID;
|
||||
}
|
||||
|
||||
public static float diffuseLight(int normal) {
|
||||
final var nx = unpackX(normal);
|
||||
final var ny = unpackY(normal);
|
||||
final var nz = unpackZ(normal);
|
||||
return Math.min(nx * nx * 0.6F + ny * ny * ((3.0F + ny) / 4.0F) + nz * nz * 0.8F, 1.0F);
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.gtnewhorizon.gtnhlib.GTNHLib;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.json.JsonModel;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadProvider;
|
||||
import com.gtnewhorizon.gtnhlib.util.Callback;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
/**
|
||||
* Model loading is as follows:
|
||||
* <ul>
|
||||
* <li>Models to load are registered in PREINIT or INIT. See {@link #registerModels(Callback, Collection)}.</li>
|
||||
* <li>A resource rebuild is triggered, then registered models and their parents are recursively loaded in POSTINIT.
|
||||
* </li>
|
||||
* <li>Models registered for baking are baked on the first client tick.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* As for icons, register them in {@link Block#registerBlockIcons}. Whatever gets them in the block texture atlas.
|
||||
*/
|
||||
@Beta
|
||||
public class ModelLoader {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(JsonModel.class, new JsonModel.Deserializer()).create();
|
||||
private static final List<ResourceLocation> unloadedModels = new ObjectArrayList<>();
|
||||
private static final Map<ResourceLocation, JsonModel> loadedModels = new Object2ObjectOpenHashMap<>();
|
||||
private static final Map<Variant, JsonModel> modelsToBake = new Object2ObjectOpenHashMap<>();
|
||||
private static final List<Callback> postBakeCallbacks = new ObjectArrayList<>();
|
||||
|
||||
/**
|
||||
* Convenience method to register a baker callback, which will be run after JSON models are baked. Useful if you
|
||||
* implemented your own non-JSON model that still needs baking.
|
||||
*/
|
||||
public static void registerBaker(Callback baker) {
|
||||
|
||||
postBakeCallbacks.add(baker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to register multiple variants. See {@link #registerModels(Callback, Collection)}.
|
||||
*/
|
||||
public static void registerModels(Callback loader, Variant... variants) {
|
||||
|
||||
registerModels(loader, Arrays.asList(variants));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass the variant(s) you want to load, and if needed a callback which puts the models in a useful place, e.g.
|
||||
* saving them to a static field after baking.
|
||||
*/
|
||||
public static void registerModels(Callback loader, Collection<Variant> variants) {
|
||||
|
||||
for (Variant v : variants) {
|
||||
|
||||
unloadedModels.add(v.getModel());
|
||||
modelsToBake.put(v, null);
|
||||
}
|
||||
|
||||
postBakeCallbacks.add(loader);
|
||||
}
|
||||
|
||||
public static void loadModels() {
|
||||
|
||||
for (ResourceLocation l : unloadedModels) {
|
||||
|
||||
if (l == null) continue;
|
||||
if (loadedModels.containsKey(l)) continue;
|
||||
|
||||
final JsonModel model = loadJson(l, JsonModel.class);
|
||||
unloadedModels.addAll(model.getParents());
|
||||
loadedModels.put(l, model);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T loadJson(ResourceLocation path, Class<T> clazz) {
|
||||
try {
|
||||
|
||||
final InputStream is = Minecraft.getMinecraft().getResourceManager().getResource(path).getInputStream();
|
||||
return GSON.fromJson(new InputStreamReader(is), clazz);
|
||||
} catch (IOException e) {
|
||||
|
||||
GTNHLib.LOG.fatal("Could not find " + path.getResourceDomain() + " " + path.getResourcePath());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bakeModels() {
|
||||
|
||||
for (Map.Entry<Variant, JsonModel> l : modelsToBake.entrySet()) {
|
||||
|
||||
final JsonModel dough = new JsonModel(loadedModels.get(l.getKey().getModel()));
|
||||
|
||||
// Resolve the parent chain
|
||||
// noinspection SuspiciousMethodCalls
|
||||
dough.resolveParents(loadedModels::get);
|
||||
|
||||
// Bake
|
||||
dough.bake(l.getKey());
|
||||
|
||||
l.setValue(dough);
|
||||
}
|
||||
|
||||
for (Callback c : postBakeCallbacks) c.run();
|
||||
}
|
||||
|
||||
public static QuadProvider getModel(Variant loc) {
|
||||
return modelsToBake.get(loc);
|
||||
}
|
||||
|
||||
public static boolean shouldLoadModels() {
|
||||
return !unloadedModels.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.json.FaceRewindHelper.DOWN;
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.json.FaceRewindHelper.EAST;
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.json.FaceRewindHelper.NORTH;
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.json.FaceRewindHelper.SOUTH;
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.json.FaceRewindHelper.UP;
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.json.FaceRewindHelper.WEST;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.util.IIcon;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Quad;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadBuilder;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.properties.ModelQuadFlags;
|
||||
|
||||
import lombok.Setter;
|
||||
|
||||
public class NdQuadBuilder extends Quad implements QuadBuilder {
|
||||
|
||||
private ForgeDirection nominalFace = ForgeDirection.UNKNOWN;
|
||||
// Defaults to UP, but only because it can't be UNKNOWN or null
|
||||
private ForgeDirection lightFace = ForgeDirection.UP;
|
||||
private int geometryFlags = 0;
|
||||
private boolean isGeometryInvalid = true;
|
||||
private int tag = 0;
|
||||
final Vector3f faceNormal = new Vector3f();
|
||||
public final Material mat = new Material();
|
||||
@Setter
|
||||
private int drawMode = GL11.GL_QUADS;
|
||||
|
||||
@Override
|
||||
public QuadView build(QuadView out) {
|
||||
|
||||
if (this.drawMode != GL11.GL_QUADS) this.quadrangulate();
|
||||
|
||||
// FRAPI does this late, but we need to do it before baking to Nd quads
|
||||
this.computeGeometry();
|
||||
out.copyFrom(this);
|
||||
this.clear();
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link #build(QuadView)}. This rotates the output by the given matrix.
|
||||
*/
|
||||
public QuadView build(QuadView out, Matrix4f rotMat) {
|
||||
|
||||
this.pos(0, this.pos(0).mulPosition(rotMat));
|
||||
this.pos(1, this.pos(1).mulPosition(rotMat));
|
||||
this.pos(2, this.pos(2).mulPosition(rotMat));
|
||||
|
||||
if (this.drawMode == GL11.GL_QUADS) this.pos(3, this.pos(3).mulPosition(rotMat));
|
||||
else this.quadrangulate();
|
||||
|
||||
this.computeGeometry();
|
||||
|
||||
// Reset the cull face
|
||||
this.setCullFace();
|
||||
|
||||
out.copyFrom(this);
|
||||
this.clear();
|
||||
return out;
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
|
||||
Arrays.fill(this.data, 0);
|
||||
this.setCullFace(ForgeDirection.UNKNOWN);
|
||||
this.lightFace = ForgeDirection.UP;
|
||||
this.geometryFlags = 0;
|
||||
this.isGeometryInvalid = true;
|
||||
this.tag(0);
|
||||
this.setColorIndex(-1);
|
||||
this.mat.reset();
|
||||
this.drawMode = GL11.GL_QUADS;
|
||||
}
|
||||
|
||||
private void computeGeometry() {
|
||||
if (this.isGeometryInvalid) {
|
||||
this.isGeometryInvalid = false;
|
||||
|
||||
NormalHelper.computeFaceNormal(this.faceNormal, this);
|
||||
|
||||
// depends on face normal
|
||||
this.lightFace = GeometryHelper.lightFace(this);
|
||||
|
||||
// depends on light face
|
||||
this.geometryFlags = ModelQuadFlags.getQuadFlags(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShade() {
|
||||
return this.mat.getDiffuse();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public ForgeDirection getLightFace() {
|
||||
this.computeGeometry();
|
||||
return this.lightFace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCullFace(ForgeDirection dir) {
|
||||
super.setCullFace(dir);
|
||||
this.nominalFace(dir);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCullFace() {
|
||||
this.computeGeometry();
|
||||
if ((this.geometryFlags & ModelQuadFlags.IS_ALIGNED) != 0) this.setCullFace(this.lightFace);
|
||||
else {
|
||||
this.setCullFace(ForgeDirection.UNKNOWN);
|
||||
this.nominalFace(this.lightFace);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nominalFace(@Nullable ForgeDirection face) {
|
||||
this.nominalFace = face;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForgeDirection nominalFace() {
|
||||
return this.nominalFace;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pos(int vertexIndex, float x, float y, float z) {
|
||||
|
||||
this.setX(vertexIndex, x);
|
||||
this.setY(vertexIndex, y);
|
||||
this.setZ(vertexIndex, z);
|
||||
isGeometryInvalid = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pos(int vertexIndex, Vector3f vec) {
|
||||
|
||||
this.setX(vertexIndex, vec.x);
|
||||
this.setY(vertexIndex, vec.y);
|
||||
this.setZ(vertexIndex, vec.z);
|
||||
isGeometryInvalid = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector3f pos(int vertexIndex) {
|
||||
|
||||
return new Vector3f(this.getX(vertexIndex), this.getY(vertexIndex), this.getZ(vertexIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public float posByIndex(int vertexIndex, int coordinateIndex) {
|
||||
return Float.intBitsToFloat(this.data[vertexIndex * Quad.VERTEX_STRIDE + Quad.X_INDEX + coordinateIndex]);
|
||||
}
|
||||
|
||||
final int MIN_X = 0;
|
||||
final int MIN_Y = 1;
|
||||
final int MIN_Z = 2;
|
||||
final int MAX_X = 3;
|
||||
final int MAX_Y = 4;
|
||||
final int MAX_Z = 5;
|
||||
|
||||
/**
|
||||
* Rewinds the quad to the standard order. Works as long as four vertex positions have been assigned, and will not
|
||||
* rewind any non-position data. Recommended to do immediately after vertex assignment, and to set/reset UVs,
|
||||
* colors, etc. afterwards.
|
||||
*
|
||||
* PS: this should only be used on quads parallel to the block grid.
|
||||
*
|
||||
* TODO: this doesn't actually work
|
||||
*/
|
||||
public void rewind(float x, float y, float z, float X, float Y, float Z) {
|
||||
boolean[] targets = switch (getLightFace()) {
|
||||
case DOWN -> DOWN;
|
||||
case UP -> UP;
|
||||
case NORTH -> NORTH;
|
||||
case SOUTH -> SOUTH;
|
||||
case WEST -> WEST;
|
||||
case EAST -> EAST;
|
||||
case UNKNOWN -> throw new RuntimeException("Expected non-UNKNOWN face!");
|
||||
};
|
||||
|
||||
final float ox = getX(0);
|
||||
final float oy = getY(0);
|
||||
final float oz = getZ(0);
|
||||
|
||||
// Reset the vertices, given the bounds
|
||||
int iFirst = 0;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
final float px = targets[i * 3] ? X : x;
|
||||
final float py = targets[i * 3 + 1] ? Y : y;
|
||||
final float pz = targets[i * 3 + 2] ? Z : z;
|
||||
pos(i, px, py, pz);
|
||||
|
||||
// Save the original first vertex
|
||||
if (px == ox && py == oy && pz == oz) iFirst = i;
|
||||
}
|
||||
|
||||
// Shift the UVs - copy to a temp array, then copy back in the right order, wrapping appropriately
|
||||
float[] uvs = { getTexU(0), getTexV(0), getTexU(1), getTexV(1), getTexU(2), getTexV(2), getTexU(3),
|
||||
getTexV(3) };
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
uv((i + iFirst) % 4, uvs[i * 2], uvs[i * 2 + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spriteBake(String spriteName, int bakeFlags) {
|
||||
|
||||
final IIcon icon = Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(spriteName);
|
||||
TexHelper.bakeSprite(this, icon, bakeFlags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spriteBake(IIcon sprite, int bakeFlags) {
|
||||
TexHelper.bakeSprite(this, sprite, bakeFlags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void square(ForgeDirection nominalFace, float left, float bottom, float right, float top, float depth) {
|
||||
if (Math.abs(depth) < CULL_FACE_EPSILON) {
|
||||
setCullFace(nominalFace);
|
||||
depth = 0; // avoid any inconsistency for face quads
|
||||
} else {
|
||||
setCullFace(ForgeDirection.UNKNOWN);
|
||||
}
|
||||
|
||||
nominalFace(nominalFace);
|
||||
switch (nominalFace) {
|
||||
case UP:
|
||||
depth = 1 - depth;
|
||||
top = 1 - top;
|
||||
bottom = 1 - bottom;
|
||||
|
||||
case DOWN:
|
||||
pos(0, left, depth, top);
|
||||
pos(1, left, depth, bottom);
|
||||
pos(2, right, depth, bottom);
|
||||
pos(3, right, depth, top);
|
||||
break;
|
||||
|
||||
case EAST:
|
||||
depth = 1 - depth;
|
||||
left = 1 - left;
|
||||
right = 1 - right;
|
||||
|
||||
case WEST:
|
||||
pos(0, depth, top, left);
|
||||
pos(1, depth, bottom, left);
|
||||
pos(2, depth, bottom, right);
|
||||
pos(3, depth, top, right);
|
||||
break;
|
||||
|
||||
case SOUTH:
|
||||
depth = 1 - depth;
|
||||
left = 1 - left;
|
||||
right = 1 - right;
|
||||
|
||||
case NORTH:
|
||||
pos(0, 1 - left, top, depth);
|
||||
pos(1, 1 - left, bottom, depth);
|
||||
pos(2, 1 - right, bottom, depth);
|
||||
pos(3, 1 - right, top, depth);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public int tag() {
|
||||
return this.tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tag(int tag) {
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uv(int vertexIndex, float u, float v) {
|
||||
|
||||
this.setTexU(vertexIndex, u);
|
||||
this.setTexV(vertexIndex, v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFlags() {
|
||||
return this.geometryFlags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextureAtlasSprite rubidium$getSprite() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector3i;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.util.DirectionUtil;
|
||||
|
||||
public class NormalHelper {
|
||||
|
||||
/**
|
||||
* Computes the face normal of the given quad and saves it in the provided non-null vector. If
|
||||
* {@link NdQuadBuilder#nominalFace()} is set will optimize by confirming quad is parallel to that face and, if so,
|
||||
* use the standard normal for that face direction.
|
||||
*
|
||||
* <p>
|
||||
* Will work with triangles also. Assumes counter-clockwise winding order, which is the norm. Expects convex quads
|
||||
* with all points co-planar.
|
||||
*/
|
||||
public static void computeFaceNormal(@NotNull Vector3f saveTo, NdQuadBuilder q) {
|
||||
final ForgeDirection nominalFace = q.nominalFace();
|
||||
|
||||
if (nominalFace != ForgeDirection.UNKNOWN && GeometryHelper.isQuadParallelToFace(nominalFace, q)) {
|
||||
Vector3i vec = DirectionUtil.STEP[nominalFace.ordinal()];
|
||||
saveTo.set(vec.x, vec.y, vec.z);
|
||||
return;
|
||||
}
|
||||
|
||||
final float x0 = q.getX(0);
|
||||
final float y0 = q.getY(0);
|
||||
final float z0 = q.getZ(0);
|
||||
final float x1 = q.getX(1);
|
||||
final float y1 = q.getY(1);
|
||||
final float z1 = q.getZ(1);
|
||||
final float x2 = q.getX(2);
|
||||
final float y2 = q.getY(2);
|
||||
final float z2 = q.getZ(2);
|
||||
final float x3 = q.getX(3);
|
||||
final float y3 = q.getY(3);
|
||||
final float z3 = q.getZ(3);
|
||||
|
||||
final float dx0 = x2 - x0;
|
||||
final float dy0 = y2 - y0;
|
||||
final float dz0 = z2 - z0;
|
||||
final float dx1 = x3 - x1;
|
||||
final float dy1 = y3 - y1;
|
||||
final float dz1 = z3 - z1;
|
||||
|
||||
float normX = dy0 * dz1 - dz0 * dy1;
|
||||
float normY = dz0 * dx1 - dx0 * dz1;
|
||||
float normZ = dx0 * dy1 - dy0 * dx1;
|
||||
|
||||
float l = (float) Math.sqrt(normX * normX + normY * normY + normZ * normZ);
|
||||
|
||||
if (l != 0) {
|
||||
normX /= l;
|
||||
normY /= l;
|
||||
normZ /= l;
|
||||
}
|
||||
|
||||
saveTo.set(normX, normY, normZ);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import net.minecraft.util.IIcon;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadBuilder;
|
||||
|
||||
public class TexHelper {
|
||||
|
||||
private static final float NORMALIZER = 1f / 16f;
|
||||
private static final VertexModifier[] UVLOCKERS = new VertexModifier[6];
|
||||
|
||||
static {
|
||||
UVLOCKERS[ForgeDirection.EAST.ordinal()] = (q, i) -> q.uv(i, 1 - q.getZ(i), 1 - q.getY(i));
|
||||
UVLOCKERS[ForgeDirection.WEST.ordinal()] = (q, i) -> q.uv(i, q.getZ(i), 1 - q.getY(i));
|
||||
UVLOCKERS[ForgeDirection.NORTH.ordinal()] = (q, i) -> q.uv(i, 1 - q.getX(i), 1 - q.getY(i));
|
||||
UVLOCKERS[ForgeDirection.SOUTH.ordinal()] = (q, i) -> q.uv(i, q.getX(i), 1 - q.getY(i));
|
||||
UVLOCKERS[ForgeDirection.DOWN.ordinal()] = (q, i) -> q.uv(i, q.getX(i), 1 - q.getZ(i));
|
||||
UVLOCKERS[ForgeDirection.UP.ordinal()] = (q, i) -> q.uv(i, q.getX(i), q.getZ(i));
|
||||
}
|
||||
|
||||
private static final VertexModifier[] ROTATIONS = new VertexModifier[] { null,
|
||||
(q, i) -> q.uv(i, q.getTexV(i), 1 - q.getTexU(i)), // 90
|
||||
(q, i) -> q.uv(i, 1 - q.getTexU(i), 1 - q.getTexV(i)), // 180
|
||||
(q, i) -> q.uv(i, 1 - q.getTexV(i), q.getTexU(i)) // 270
|
||||
};
|
||||
|
||||
private static void applyModifier(NdQuadBuilder quad, VertexModifier modifier) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
modifier.apply(quad, i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bakes textures in the provided vertex data, handling UV locking, rotation, interpolation, etc. Textures must not
|
||||
* be already baked.
|
||||
*/
|
||||
public static void bakeSprite(NdQuadBuilder quad, IIcon sprite, int bakeFlags) {
|
||||
if (quad.nominalFace() != ForgeDirection.UNKNOWN && (QuadBuilder.BAKE_LOCK_UV & bakeFlags) != 0) {
|
||||
// Assigns normalized UV coordinates based on vertex positions
|
||||
applyModifier(quad, UVLOCKERS[quad.nominalFace().ordinal()]);
|
||||
} else if ((QuadBuilder.BAKE_NORMALIZED & bakeFlags) == 0) { // flag is NOT set, UVs are assumed to not be
|
||||
// normalized yet as is the default, normalize
|
||||
// through dividing by 16
|
||||
// Scales from 0-16 to 0-1
|
||||
applyModifier(quad, (q, i) -> q.uv(i, q.getTexU(i) * NORMALIZER, q.getTexV(i) * NORMALIZER));
|
||||
}
|
||||
|
||||
final int rotation = bakeFlags & 3;
|
||||
|
||||
if (rotation != 0) {
|
||||
// Rotates texture around the center of sprite.
|
||||
// Assumes normalized coordinates.
|
||||
applyModifier(quad, ROTATIONS[rotation]);
|
||||
}
|
||||
|
||||
if ((QuadBuilder.BAKE_FLIP_U & bakeFlags) != 0) {
|
||||
// Inverts U coordinates. Assumes normalized (0-1) values.
|
||||
applyModifier(quad, (q, i) -> q.uv(i, 1 - q.getTexU(i), q.getTexV(i)));
|
||||
}
|
||||
|
||||
if ((QuadBuilder.BAKE_FLIP_V & bakeFlags) != 0) {
|
||||
// Inverts V coordinates. Assumes normalized (0-1) values.
|
||||
applyModifier(quad, (q, i) -> q.uv(i, q.getTexU(i), 1 - q.getTexV(i)));
|
||||
}
|
||||
|
||||
interpolate(quad, sprite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Faster than sprite method. Sprite computes span and normalizes inputs each call, so we'd have to denormalize
|
||||
* before we called, only to have the sprite renormalize immediately.
|
||||
*/
|
||||
private static void interpolate(NdQuadBuilder q, IIcon sprite) {
|
||||
final float uMin = sprite.getMinU();
|
||||
final float uSpan = sprite.getMaxU() - uMin;
|
||||
final float vMin = sprite.getMinV();
|
||||
final float vSpan = sprite.getMaxV() - vMin;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
q.uv(i, uMin + q.getTexU(i) * uSpan, vMin + q.getTexV(i) * vSpan);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface VertexModifier {
|
||||
|
||||
void apply(NdQuadBuilder quad, int vertexIndex);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import static java.lang.Math.toRadians;
|
||||
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Beta
|
||||
public class Variant {
|
||||
|
||||
@Getter
|
||||
private final ResourceLocation model;
|
||||
private final float x;
|
||||
private final float y;
|
||||
private final float z;
|
||||
private final boolean uvLock;
|
||||
|
||||
public Variant(ResourceLocation model, int x, int y, boolean uvLock) {
|
||||
this(model, x, y, 0, uvLock);
|
||||
}
|
||||
|
||||
public Variant(ResourceLocation model, int x, int y, int z, boolean uvLock) {
|
||||
this.model = model;
|
||||
this.x = (float) toRadians(x);
|
||||
this.y = (float) toRadians(y);
|
||||
this.z = (float) toRadians(z);
|
||||
this.uvLock = uvLock;
|
||||
}
|
||||
|
||||
public Matrix4f getAffineMatrix() {
|
||||
|
||||
return new Matrix4f().translation(-.5f, -.5f, -.5f).rotateLocalX(x).rotateLocalY(y).rotateLocalZ(z)
|
||||
.translateLocal(.5f, .5f, .5f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
|
||||
/// A simple wrapper for any object, adding a weight. Also provides a helper for selecting from a weighted list.
|
||||
public final class Weighted<T> {
|
||||
|
||||
private final T thing;
|
||||
private final int weight;
|
||||
|
||||
public Weighted(T thing, int weight) {
|
||||
this.thing = thing;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
public static <T> T selectOne(ObjectList<Weighted<T>> heavyThings, Random rand) {
|
||||
var weight = 0;
|
||||
for (var v : heavyThings) {
|
||||
weight += v.weight();
|
||||
}
|
||||
|
||||
final var selector = rand.nextInt(weight);
|
||||
weight = 0;
|
||||
for (var v : heavyThings) {
|
||||
if (selector <= weight) return v.thing();
|
||||
weight += v.weight();
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Randomly selected beyond the list!");
|
||||
}
|
||||
|
||||
public T thing() {
|
||||
return thing;
|
||||
}
|
||||
|
||||
public int weight() {
|
||||
return weight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.baked;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
|
||||
|
||||
public interface BakedModel {
|
||||
|
||||
int R_MASK = 0xFF << 16;
|
||||
int G_MASK = 0xFF << 8;
|
||||
int B_MASK = 0xFF;
|
||||
|
||||
/// Called once per block and returns the color packed as ABGR. If you want to vary colors within a block, just
|
||||
/// ignore the color passed into [#getQuads] and use your own function, don't override this. But if you only
|
||||
/// want to change color once per block, like vanilla leaves, override this. The default vanilla adapter is
|
||||
/// [#getDefaultColor]
|
||||
default int getColor(IBlockAccess world, int x, int y, int z, Block block, int meta, Random random) {
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// Defers to the vanilla color functions, just repacks the color to ABGR as models require.
|
||||
static int getDefaultColor(IBlockAccess world, int x, int y, int z, Block block) {
|
||||
|
||||
final int cin = block.colorMultiplier(world, x, y, z);
|
||||
return (0xFF << 24) | ((cin & B_MASK) << 16) | (cin & G_MASK) | ((cin & R_MASK) >>> 16);
|
||||
}
|
||||
|
||||
/// If you need to allocate new quads, set this to true. Then, the quads returned by [#getQuads] are recycled,
|
||||
/// and you should not keep a reference to them. Example: stone can return a static list every time, but a modded
|
||||
/// block which adds or removes quads based on location would likely need dynamic quads.
|
||||
default boolean isDynamic() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Provide quads to render. If you need new quads, they should be obtained with the passed supplier,
|
||||
/// [#isDynamic] should be overridden to return true, and you should not keep references to the quads.
|
||||
///
|
||||
/// @param world The world the model is in. Null if not applicable, like in inventory rendering.
|
||||
/// @param x The x-position of the model.
|
||||
/// @param y The y-position of the model.
|
||||
/// @param z The z-position of the model.
|
||||
/// @param block The block to be rendered.
|
||||
/// @param meta The meta value of the rendered object.
|
||||
/// @param dir The direction to get quads from - the returned quads should have the same facing.
|
||||
/// @param random A RNG to use.
|
||||
/// @param color The ABGR packed color to use.
|
||||
/// @param quadPool If [#isDynamic()], the pool to obtain quads from and where the quads should be released
|
||||
/// after use. Ignored otherwise.
|
||||
/// @return A list of quads from the model. These are *not* adjusted for the given xyz - that is up to the renderer.
|
||||
List<ModelQuadView> getQuads(@Nullable IBlockAccess world, int x, int y, int z, Block block, int meta,
|
||||
ModelQuadFacing dir, Random random, int color, @Nullable Supplier<ModelQuadViewMutable> quadPool);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.baked;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.Weighted;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
public final class MonopartModel implements BakedModel {
|
||||
|
||||
private final ObjectArrayList<Weighted<BakedModel>> models;
|
||||
|
||||
public MonopartModel(ObjectArrayList<Weighted<BakedModel>> models) {
|
||||
this.models = models;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ModelQuadView> getQuads(@Nullable IBlockAccess world, int x, int y, int z, Block block, int meta,
|
||||
ModelQuadFacing dir, Random random, int color, @Nullable Supplier<ModelQuadViewMutable> quadPool) {
|
||||
return Weighted.selectOne(models, random).getQuads(world, x, y, z, block, meta, dir, random, color, quadPool);
|
||||
}
|
||||
|
||||
public ObjectArrayList<Weighted<BakedModel>> models() {
|
||||
return models;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.baked;
|
||||
|
||||
import static it.unimi.dsi.fastutil.objects.Object2ObjectMaps.unmodifiable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.MultipartState.Case.Condition;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps.UnmodifiableMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
public final class MultipartModel implements BakedModel {
|
||||
|
||||
private final UnmodifiableMap<Condition, BakedModel> piles;
|
||||
|
||||
public MultipartModel(UnmodifiableMap<Condition, BakedModel> piles) {
|
||||
this.piles = piles;
|
||||
}
|
||||
|
||||
public MultipartModel(Object2ObjectArrayMap<Condition, BakedModel> piles) {
|
||||
this((UnmodifiableMap<Condition, BakedModel>) unmodifiable(piles));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ModelQuadView> getQuads(@Nullable IBlockAccess world, int x, int y, int z, Block block, int meta,
|
||||
ModelQuadFacing dir, Random random, int color, @Nullable Supplier<ModelQuadViewMutable> quadPool) {
|
||||
final var quads = new ObjectArrayList<ModelQuadView>();
|
||||
Object2ObjectMaps.fastForEach(piles, e -> {
|
||||
if (e.getKey().matches(stateMap(meta)))
|
||||
quads.addAll(e.getValue().getQuads(world, x, y, z, block, meta, dir, random, color, quadPool));
|
||||
});
|
||||
return quads;
|
||||
}
|
||||
|
||||
private static Map<String, String> stateMap(int meta) {
|
||||
final var m = new HashMap<String, String>();
|
||||
m.put("meta", Integer.toString(meta));
|
||||
return m;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.baked;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.NEG_X;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.NEG_Y;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.NEG_Z;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.POS_X;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.POS_Y;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.POS_Z;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.UNASSIGNED;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
|
||||
|
||||
public final class PileOfQuads implements BakedModel {
|
||||
|
||||
public static final PileOfQuads BLANK = new PileOfQuads(new HashMap<>());
|
||||
private final SidedQuadStore sidedQuadStore;
|
||||
|
||||
public PileOfQuads(SidedQuadStore sidedQuadStore) {
|
||||
this.sidedQuadStore = sidedQuadStore;
|
||||
}
|
||||
|
||||
public PileOfQuads(Map<ModelQuadFacing, ArrayList<ModelQuadView>> sidedQuadStore) {
|
||||
this(new SidedQuadStore(sidedQuadStore));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ModelQuadView> getQuads(@Nullable IBlockAccess world, int x, int y, int z, Block block, int meta,
|
||||
ModelQuadFacing dir, Random random, int color, @Nullable Supplier<ModelQuadViewMutable> quadPool) {
|
||||
return sidedQuadStore.getQuads(dir);
|
||||
}
|
||||
|
||||
public static class SidedQuadStore {
|
||||
|
||||
private static final ObjectImmutableList<ModelQuadView> EMPTY = ObjectImmutableList.of();
|
||||
|
||||
private final ObjectImmutableList<ModelQuadView> up;
|
||||
private final ObjectImmutableList<ModelQuadView> down;
|
||||
private final ObjectImmutableList<ModelQuadView> north;
|
||||
private final ObjectImmutableList<ModelQuadView> south;
|
||||
private final ObjectImmutableList<ModelQuadView> west;
|
||||
private final ObjectImmutableList<ModelQuadView> east;
|
||||
private final ObjectImmutableList<ModelQuadView> unknown;
|
||||
|
||||
public SidedQuadStore(Map<ModelQuadFacing, ArrayList<ModelQuadView>> sidedQuadStore) {
|
||||
up = lockList(sidedQuadStore.get(POS_Y));
|
||||
down = lockList(sidedQuadStore.get(NEG_Y));
|
||||
north = lockList(sidedQuadStore.get(NEG_Z));
|
||||
south = lockList(sidedQuadStore.get(POS_Z));
|
||||
west = lockList(sidedQuadStore.get(NEG_X));
|
||||
east = lockList(sidedQuadStore.get(POS_X));
|
||||
unknown = lockList(sidedQuadStore.get(UNASSIGNED));
|
||||
}
|
||||
|
||||
public List<ModelQuadView> getQuads(ModelQuadFacing dir) {
|
||||
return switch (dir) {
|
||||
case POS_Y -> up;
|
||||
case NEG_Y -> down;
|
||||
case NEG_Z -> north;
|
||||
case POS_Z -> south;
|
||||
case NEG_X -> west;
|
||||
case POS_X -> east;
|
||||
case UNASSIGNED -> unknown;
|
||||
};
|
||||
}
|
||||
|
||||
private ObjectImmutableList<ModelQuadView> lockList(@Nullable List<ModelQuadView> list) {
|
||||
if (list == null || list.isEmpty()) return EMPTY;
|
||||
return new ObjectImmutableList<>(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.json;
|
||||
|
||||
/**
|
||||
* Modern vanilla uses magic numbers to rewind quads, let's try that. NOTE: may or may not work with sheared quads, but
|
||||
* probably not.
|
||||
*/
|
||||
public class FaceRewindHelper {
|
||||
|
||||
/**
|
||||
* These constants map face sides to the characteristics of the vertexes. False is min, true is max, and the order
|
||||
* is XYZ, repeating 4 times. So the first vertex of DOWN faces should have the minimum X and Y and the maximum Z.
|
||||
*/
|
||||
public static final boolean[] DOWN = { false, false, true, false, false, false, true, false, false, true, false,
|
||||
true };
|
||||
public static final boolean[] UP = { false, true, false, false, true, true, true, true, true, true, true, false };
|
||||
public static final boolean[] NORTH = { true, true, false, true, false, false, false, false, false, false, true,
|
||||
false };
|
||||
public static final boolean[] SOUTH = { false, true, true, false, false, true, true, false, true, true, true,
|
||||
true };
|
||||
public static final boolean[] WEST = { false, true, false, false, false, false, false, false, true, false, true,
|
||||
true };
|
||||
public static final boolean[] EAST = { true, true, true, true, false, true, true, false, false, true, true, false };
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.json;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.util.DirectionUtil.ALL_DIRECTIONS;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.IBlockPos;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.NdQuadBuilder;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.Variant;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Axis;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Quad;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadBuilder;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadProvider;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.util.DirectionUtil;
|
||||
import com.gtnewhorizon.gtnhlib.util.JsonUtil;
|
||||
|
||||
import it.unimi.dsi.fastutil.Function;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
|
||||
import lombok.Getter;
|
||||
|
||||
public class JsonModel implements QuadProvider {
|
||||
|
||||
@Nullable
|
||||
private final ResourceLocation parentId;
|
||||
@Nullable
|
||||
private JsonModel parent;
|
||||
@Getter
|
||||
private final boolean useAO;
|
||||
private final Map<ModelDisplay.Position, ModelDisplay> display;
|
||||
private final Map<String, String> textures;
|
||||
private List<ModelElement> elements;
|
||||
private List<QuadView> allQuadStore = new ObjectArrayList<>();
|
||||
private final Map<ForgeDirection, List<QuadView>> sidedQuadStore = new Object2ObjectOpenHashMap<>();
|
||||
private static final List<QuadView> EMPTY = ObjectImmutableList.of();
|
||||
|
||||
public JsonModel(@Nullable ResourceLocation parentId, boolean useAO,
|
||||
Map<ModelDisplay.Position, ModelDisplay> display, Map<String, String> textures,
|
||||
List<ModelElement> elements) {
|
||||
this.parentId = parentId;
|
||||
this.useAO = useAO;
|
||||
this.display = display;
|
||||
this.textures = textures;
|
||||
this.elements = elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a shallow copy of og. This allows you to bake the same model multiple times with various transformations.
|
||||
*/
|
||||
public JsonModel(JsonModel og) {
|
||||
|
||||
this.parentId = og.parentId;
|
||||
this.parent = og.parent;
|
||||
this.useAO = og.useAO;
|
||||
this.display = og.display;
|
||||
this.textures = og.textures;
|
||||
this.elements = og.elements;
|
||||
}
|
||||
|
||||
public void bake(Variant v) {
|
||||
|
||||
final Matrix4f vRot = v.getAffineMatrix();
|
||||
final NdQuadBuilder builder = new NdQuadBuilder();
|
||||
|
||||
// Append faces from each element
|
||||
for (ModelElement e : this.elements) {
|
||||
|
||||
final Matrix4f rot = (e.getRotation() == null) ? ModelElement.Rotation.NOOP.getAffineMatrix()
|
||||
: e.getRotation().getAffineMatrix();
|
||||
|
||||
final Vector3f from = e.getFrom();
|
||||
final Vector3f to = e.getTo();
|
||||
|
||||
for (ModelElement.Face f : e.getFaces()) {
|
||||
|
||||
float x = Float.MAX_VALUE;
|
||||
float y = Float.MAX_VALUE;
|
||||
float z = Float.MAX_VALUE;
|
||||
float X = Float.MIN_VALUE;
|
||||
float Y = Float.MIN_VALUE;
|
||||
float Z = Float.MIN_VALUE;
|
||||
|
||||
// Assign vertexes
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
|
||||
final Vector3f vert = QuadBuilder.mapSideToVertex(from, to, i, f.getName()).mulPosition(rot)
|
||||
.mulPosition(vRot);
|
||||
builder.pos(i, vert.x, vert.y, vert.z);
|
||||
x = min(x, vert.x);
|
||||
y = min(y, vert.y);
|
||||
z = min(z, vert.z);
|
||||
X = max(X, vert.x);
|
||||
Y = max(Y, vert.y);
|
||||
Z = max(Z, vert.z);
|
||||
}
|
||||
|
||||
// Set culling and nominal faces
|
||||
builder.setCullFace();
|
||||
|
||||
// Set bake flags
|
||||
int flags = switch (f.getRotation()) {
|
||||
case 90 -> QuadBuilder.BAKE_ROTATE_90;
|
||||
case 180 -> QuadBuilder.BAKE_ROTATE_180;
|
||||
case 270 -> QuadBuilder.BAKE_ROTATE_270;
|
||||
default -> QuadBuilder.BAKE_ROTATE_NONE;
|
||||
};
|
||||
|
||||
// Set UV
|
||||
// TODO: UV locking
|
||||
final Vector4f uv = f.getUv();
|
||||
if (uv != null) {
|
||||
|
||||
builder.uv(0, uv.x, uv.y);
|
||||
builder.uv(1, uv.x, uv.w);
|
||||
builder.uv(2, uv.z, uv.w);
|
||||
builder.uv(3, uv.z, uv.y);
|
||||
} else {
|
||||
|
||||
// Not sure if this is correct, but it seems to fix things
|
||||
flags |= QuadBuilder.BAKE_LOCK_UV;
|
||||
}
|
||||
|
||||
// Set the sprite
|
||||
builder.spriteBake(this.textures.get(f.getTexture()), flags);
|
||||
|
||||
// Set the tint index
|
||||
builder.setColors(f.getTintIndex());
|
||||
|
||||
// Set AO
|
||||
builder.mat.setAO(this.useAO);
|
||||
|
||||
// Bake and add it
|
||||
final QuadView q = builder.build(new Quad());
|
||||
this.allQuadStore.add(q);
|
||||
this.sidedQuadStore.computeIfAbsent(q.getCullFace(), o -> new ObjectArrayList<>()).add(q);
|
||||
}
|
||||
}
|
||||
|
||||
// Lock the lists.
|
||||
this.allQuadStore = new ObjectImmutableList<>(this.allQuadStore);
|
||||
for (ForgeDirection f : ALL_DIRECTIONS) {
|
||||
|
||||
List<QuadView> l = this.sidedQuadStore.computeIfAbsent(f, o -> EMPTY);
|
||||
if (!l.isEmpty()) this.sidedQuadStore.put(f, new ObjectImmutableList<>(l));
|
||||
}
|
||||
}
|
||||
|
||||
public List<ResourceLocation> getParents() {
|
||||
return Arrays.asList(parentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuadView> getQuads(IBlockAccess world, IBlockPos pos, Block block, int meta, ForgeDirection dir,
|
||||
Random random, int color, Supplier<QuadView> quadPool) {
|
||||
|
||||
return this.sidedQuadStore.getOrDefault(dir, EMPTY);
|
||||
}
|
||||
|
||||
public void resolveParents(Function<ResourceLocation, JsonModel> modelLoader) {
|
||||
|
||||
if (this.parentId != null && this.parent == null) {
|
||||
|
||||
final JsonModel p = modelLoader.apply(this.parentId);
|
||||
p.resolveParents(modelLoader);
|
||||
|
||||
// Inherit properties
|
||||
this.parent = p;
|
||||
if (this.elements.isEmpty()) this.elements = this.parent.elements;
|
||||
|
||||
// Resolve texture variables
|
||||
// Add parent texture mappings, but prioritize ours.
|
||||
for (Map.Entry<String, String> e : this.parent.textures.entrySet()) {
|
||||
|
||||
this.textures.putIfAbsent(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
// Flatten them, merging s -> s1, s1 -> s2 to s -> s2, s1 -> s2.
|
||||
boolean flat = false;
|
||||
final Map<String, String> tmp = new Object2ObjectOpenHashMap<>();
|
||||
while (!flat) {
|
||||
flat = true;
|
||||
|
||||
for (Map.Entry<String, String> e : this.textures.entrySet()) {
|
||||
|
||||
// If there is a value in the key set, replace with the value it points to
|
||||
// Also avoid adding a loop
|
||||
if (this.textures.containsKey(e.getValue())) {
|
||||
|
||||
if (!e.getKey().equals(e.getValue())) tmp.put(e.getKey(), this.textures.get(e.getValue()));
|
||||
else tmp.put(e.getKey(), "");
|
||||
flat = false;
|
||||
} else {
|
||||
tmp.put(e.getKey(), e.getValue());
|
||||
}
|
||||
}
|
||||
this.textures.putAll(tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Deserializer implements JsonDeserializer<JsonModel> {
|
||||
|
||||
private Vector3f loadVec3(JsonObject in, String name) {
|
||||
|
||||
final JsonArray arr = in.getAsJsonArray(name);
|
||||
final Vector3f ret = new Vector3f();
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
ret.setComponent(i, arr.get(i).getAsFloat());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private Vector4f loadVec4(JsonObject in, String name) {
|
||||
|
||||
final JsonArray arr = in.getAsJsonArray(name);
|
||||
final Vector4f ret = new Vector4f();
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
ret.setComponent(i, arr.get(i).getAsFloat());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private ModelDisplay loadADisplay(JsonObject in) {
|
||||
|
||||
final Vector3f rotation = loadVec3(in, "rotation");
|
||||
final Vector3f translation = loadVec3(in, "translation");
|
||||
final Vector3f scale = loadVec3(in, "scale");
|
||||
|
||||
return new ModelDisplay(rotation, translation, scale);
|
||||
}
|
||||
|
||||
private Map<ModelDisplay.Position, ModelDisplay> loadDisplay(JsonObject in) {
|
||||
|
||||
// wow such long
|
||||
final Map<ModelDisplay.Position, ModelDisplay> ret = new Object2ObjectOpenHashMap<>(
|
||||
ModelDisplay.Position.values().length);
|
||||
|
||||
if (in.has("display")) {
|
||||
|
||||
final JsonObject display = in.getAsJsonObject("display");
|
||||
|
||||
for (Map.Entry<String, JsonElement> j : display.entrySet()) {
|
||||
|
||||
final String name = j.getKey();
|
||||
final ModelDisplay.Position pos = ModelDisplay.Position.getByName(name);
|
||||
ret.put(pos, loadADisplay(j.getValue().getAsJsonObject()));
|
||||
}
|
||||
}
|
||||
|
||||
for (ModelDisplay.Position p : ModelDisplay.Position.values()) {
|
||||
ret.putIfAbsent(p, ModelDisplay.DEFAULT);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private Map<String, String> loadTextures(JsonObject in) {
|
||||
|
||||
final Map<String, String> textures = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
if (in.has("textures")) {
|
||||
for (Map.Entry<String, JsonElement> e : in.getAsJsonObject("textures").entrySet()) {
|
||||
|
||||
// Trim leading octothorpes. They indicate a texture variable, but I don't actually care.
|
||||
String s = e.getValue().getAsString();
|
||||
if (s.startsWith("#")) {
|
||||
s = s.substring(1);
|
||||
} else if (s.startsWith("minecraft:")) {
|
||||
|
||||
// Because of how we fetch textures from the atlas, minecraft textures need to have their domain
|
||||
// stripped
|
||||
s = s.substring(10);
|
||||
}
|
||||
|
||||
textures.put(e.getKey(), s);
|
||||
}
|
||||
}
|
||||
|
||||
return textures;
|
||||
}
|
||||
|
||||
private ModelElement.Rotation loadRotation(JsonObject in) {
|
||||
|
||||
if (in.has("rotation")) {
|
||||
final JsonObject json = in.getAsJsonObject("rotation");
|
||||
|
||||
final Vector3f origin = loadVec3(json, "origin").div(16);
|
||||
final Axis axis = Axis.fromName(JsonUtil.loadStr(json, "axis"));
|
||||
final float angle = JsonUtil.loadFloat(json, "angle");
|
||||
final boolean rescale = JsonUtil.loadBool(json, "rescale", false);
|
||||
|
||||
return new ModelElement.Rotation(origin, axis, angle, rescale);
|
||||
} else {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<ModelElement.Face> loadFaces(JsonObject in) {
|
||||
|
||||
final List<ModelElement.Face> ret = new ObjectArrayList<>();
|
||||
final JsonObject json = in.getAsJsonObject("faces");
|
||||
|
||||
for (Map.Entry<String, JsonElement> e : json.entrySet()) {
|
||||
|
||||
final ForgeDirection side = DirectionUtil.fromName(e.getKey());
|
||||
final JsonObject face = e.getValue().getAsJsonObject();
|
||||
|
||||
final Vector4f uv = (face.has("uv")) ? loadVec4(face, "uv") : null;
|
||||
String texture = JsonUtil.loadStr(face, "texture");
|
||||
if (texture.startsWith("#")) texture = texture.substring(1);
|
||||
final ForgeDirection cullFace = DirectionUtil.fromName(JsonUtil.loadStr(face, "cullface", "unknown"));
|
||||
final int rotation = JsonUtil.loadInt(face, "rotation", 0);
|
||||
final int tintIndex = JsonUtil.loadInt(face, "tintindex", -1);
|
||||
|
||||
ret.add(new ModelElement.Face(side, uv, texture, cullFace, rotation, tintIndex));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private List<ModelElement> loadElements(JsonObject in) {
|
||||
|
||||
final List<ModelElement> ret = new ObjectArrayList<>();
|
||||
|
||||
if (in.has("elements")) {
|
||||
|
||||
final JsonArray arr = in.getAsJsonArray("elements");
|
||||
for (JsonElement e : arr) {
|
||||
|
||||
final JsonObject json = e.getAsJsonObject();
|
||||
final Vector3f from = loadVec3(json, "from").div(16);
|
||||
final Vector3f to = loadVec3(json, "to").div(16);
|
||||
final ModelElement.Rotation rotation = loadRotation(json);
|
||||
final boolean shade = JsonUtil.loadBool(json, "shade", true);
|
||||
final List<ModelElement.Face> faces = loadFaces(json);
|
||||
|
||||
ret.add(new ModelElement(from, to, rotation, shade, faces));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
|
||||
final JsonObject in = json.getAsJsonObject();
|
||||
|
||||
final String parent = JsonUtil.loadStr(in, "parent", "");
|
||||
ResourceLocation parentId = null;
|
||||
if (!parent.isEmpty()) {
|
||||
if (parent.contains(":")) {
|
||||
parentId = new ModelLocation(parent.split(":")[0], parent.split(":")[1]);
|
||||
} else {
|
||||
parentId = new ModelLocation(parent);
|
||||
}
|
||||
}
|
||||
|
||||
final boolean useAO = JsonUtil.loadBool(in, "ambientocclusion", true);
|
||||
final Map<ModelDisplay.Position, ModelDisplay> display = loadDisplay(in);
|
||||
final Map<String, String> textures = loadTextures(in);
|
||||
final List<ModelElement> elements = loadElements(in);
|
||||
|
||||
return new JsonModel(parentId, useAO, display, textures, elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.json;
|
||||
|
||||
import org.joml.Vector3f;
|
||||
|
||||
public class ModelDisplay {
|
||||
|
||||
public static final ModelDisplay DEFAULT = new ModelDisplay(
|
||||
new Vector3f(0, 0, 0),
|
||||
new Vector3f(0, 0, 0),
|
||||
new Vector3f(1, 1, 1));
|
||||
|
||||
private final Vector3f rotation;
|
||||
private final Vector3f translation;
|
||||
private final Vector3f scale;
|
||||
|
||||
public ModelDisplay(Vector3f rotation, Vector3f translation, Vector3f scale) {
|
||||
|
||||
this.rotation = rotation;
|
||||
this.translation = translation;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public enum Position {
|
||||
|
||||
thirdperson_righthand,
|
||||
thirdperson_lefthand,
|
||||
firstperson_righthand,
|
||||
firstperson_lefthand,
|
||||
gui,
|
||||
head,
|
||||
ground,
|
||||
fixed;
|
||||
|
||||
public static Position getByName(String name) {
|
||||
return switch (name) {
|
||||
case "thirdperson_righthand" -> thirdperson_righthand;
|
||||
case "thirdperson_lefthand" -> thirdperson_lefthand;
|
||||
case "firstperson_righthand" -> firstperson_righthand;
|
||||
case "firstperson_lefthand" -> firstperson_lefthand;
|
||||
case "gui" -> gui;
|
||||
case "head" -> head;
|
||||
case "ground" -> ground;
|
||||
case "fixed" -> fixed;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.json;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Axis;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class ModelElement {
|
||||
|
||||
@Getter
|
||||
private final Vector3f from;
|
||||
@Getter
|
||||
private final Vector3f to;
|
||||
@Nullable
|
||||
@Getter
|
||||
private final Rotation rotation;
|
||||
private final boolean shade;
|
||||
@Getter
|
||||
private final List<Face> faces;
|
||||
|
||||
ModelElement(Vector3f from, Vector3f vector3f, @Nullable Rotation rotation, boolean shade, List<Face> faces) {
|
||||
this.from = from;
|
||||
to = vector3f;
|
||||
this.rotation = rotation;
|
||||
this.shade = shade;
|
||||
this.faces = faces;
|
||||
}
|
||||
|
||||
static class Face {
|
||||
|
||||
@Getter
|
||||
private final ForgeDirection name;
|
||||
@Getter
|
||||
private final Vector4f uv;
|
||||
@Getter
|
||||
private final String texture;
|
||||
@Getter
|
||||
private final ForgeDirection cullFace;
|
||||
@Getter
|
||||
private final int rotation;
|
||||
@Getter
|
||||
private final int tintIndex;
|
||||
|
||||
Face(ForgeDirection name, Vector4f uv, String texture, ForgeDirection cullFace, int rotation, int tintIndex) {
|
||||
this.name = name;
|
||||
this.uv = uv;
|
||||
this.texture = texture;
|
||||
this.cullFace = cullFace;
|
||||
this.rotation = rotation;
|
||||
this.tintIndex = tintIndex;
|
||||
}
|
||||
}
|
||||
|
||||
static class Rotation {
|
||||
|
||||
static final Rotation NOOP = new Rotation(new Vector3f(0, 0, 0), Axis.X, 0, false);
|
||||
|
||||
private final Vector3f origin;
|
||||
private final Axis axis;
|
||||
private final float angle;
|
||||
private final boolean rescale;
|
||||
|
||||
Rotation(Vector3f origin, Axis axis, float angle, boolean rescale) {
|
||||
this.origin = origin;
|
||||
this.axis = axis;
|
||||
this.angle = (float) Math.toRadians(angle);
|
||||
this.rescale = rescale;
|
||||
}
|
||||
|
||||
Matrix4f getAffineMatrix() {
|
||||
|
||||
// Subtract origin
|
||||
final Matrix4f ret = new Matrix4f().translation(-this.origin.x, -this.origin.y, -this.origin.z);
|
||||
|
||||
// Rotate
|
||||
switch (this.axis) {
|
||||
case X -> ret.rotateLocalX(angle);
|
||||
case Y -> ret.rotateLocalY(angle);
|
||||
case Z -> ret.rotateLocalZ(angle);
|
||||
}
|
||||
|
||||
// Add the origin back in
|
||||
return ret.translateLocal(this.origin.x, this.origin.y, this.origin.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.json;
|
||||
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
public class ModelLocation extends ResourceLocation {
|
||||
|
||||
/**
|
||||
* Create a model location in the minecraft domain.
|
||||
*/
|
||||
public ModelLocation(String path) {
|
||||
super("models/" + path + ".json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a model location in any domain
|
||||
*/
|
||||
public ModelLocation(String domain, String path) {
|
||||
super(domain, "models/" + path + ".json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.loading;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.Axis.X;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.Axis.Y;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.Axis.Z;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelDeserializer.Position.ModelDisplay;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.JSONModel;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.Axis;
|
||||
import com.gtnewhorizon.gtnhlib.util.JsonUtil;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
public class ModelDeserializer implements JsonDeserializer<JSONModel> {
|
||||
|
||||
private static ForgeDirection fromName(String name) {
|
||||
return switch (name) {
|
||||
case "up" -> ForgeDirection.UP;
|
||||
case "down" -> ForgeDirection.DOWN;
|
||||
case "north" -> ForgeDirection.NORTH;
|
||||
case "south" -> ForgeDirection.SOUTH;
|
||||
case "west" -> ForgeDirection.WEST;
|
||||
case "east" -> ForgeDirection.EAST;
|
||||
case "unknown" -> ForgeDirection.UNKNOWN;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private static Vector3f loadVec3(JsonObject in, String name) {
|
||||
|
||||
final JsonArray arr = in.getAsJsonArray(name);
|
||||
final Vector3f ret = new Vector3f();
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
ret.setComponent(i, arr.get(i).getAsFloat());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static Vector3f loadVec3(JsonObject in, String name, Vector3f defaultv) {
|
||||
if (!in.isJsonArray()) return defaultv;
|
||||
return loadVec3(in, name);
|
||||
}
|
||||
|
||||
private static Vector4f loadVec4(JsonObject in, String name) {
|
||||
|
||||
final JsonArray arr = in.getAsJsonArray(name);
|
||||
final Vector4f ret = new Vector4f();
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
ret.setComponent(i, arr.get(i).getAsFloat());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static ModelDisplay loadADisplay(JsonObject in) {
|
||||
|
||||
final Vector3f rotation = loadVec3(in, "rotation", new Vector3f(0, 0, 0));
|
||||
final Vector3f translation = loadVec3(in, "translation", new Vector3f(0, 0, 0));
|
||||
final Vector3f scale = loadVec3(in, "scale", new Vector3f(1, 1, 1));
|
||||
|
||||
return new ModelDisplay(rotation, translation, scale);
|
||||
}
|
||||
|
||||
private static Map<Position, ModelDisplay> loadDisplay(JsonObject in) {
|
||||
|
||||
// wow such long
|
||||
final Map<Position, ModelDisplay> ret = new Object2ObjectOpenHashMap<>(Position.values().length);
|
||||
|
||||
if (in.has("display")) {
|
||||
|
||||
final JsonObject display = in.getAsJsonObject("display");
|
||||
|
||||
for (Map.Entry<String, JsonElement> j : display.entrySet()) {
|
||||
|
||||
final String name = j.getKey();
|
||||
final Position pos = Position.getByName(name);
|
||||
ret.put(pos, loadADisplay(j.getValue().getAsJsonObject()));
|
||||
}
|
||||
}
|
||||
|
||||
for (Position p : Position.values()) {
|
||||
ret.putIfAbsent(p, ModelDisplay.DEFAULT);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static Pattern TEXEX = Pattern.compile("^([^:]+:)block/");
|
||||
|
||||
private static Object2ObjectOpenHashMap<String, String> loadTextures(JsonObject in) {
|
||||
|
||||
final var textures = new Object2ObjectOpenHashMap<String, String>();
|
||||
|
||||
if (in.has("textures")) {
|
||||
for (Map.Entry<String, JsonElement> e : in.getAsJsonObject("textures").entrySet()) {
|
||||
String s = e.getValue().getAsString();
|
||||
|
||||
// If it's a texture variable, no munging is needed
|
||||
if (!s.startsWith("#")) {
|
||||
// Add the default domain, if it's absent.
|
||||
if (!s.contains(":")) s = "minecraft:" + s;
|
||||
|
||||
// Strip "block/" if present
|
||||
s = TEXEX.matcher(s).replaceFirst("$1");
|
||||
}
|
||||
|
||||
// The key is always a variable, so prepend accordingly
|
||||
final var key = e.getKey();
|
||||
textures.put(key.startsWith("#") ? key : "#" + key, s);
|
||||
}
|
||||
}
|
||||
|
||||
return textures;
|
||||
}
|
||||
|
||||
private static ModelElement.Rotation loadRotation(JsonObject in) {
|
||||
|
||||
if (in.has("rotation")) {
|
||||
final JsonObject json = in.getAsJsonObject("rotation");
|
||||
|
||||
final Vector3f origin = loadVec3(json, "origin").div(16);
|
||||
final String sAxis = JsonUtil.loadStr(json, "axis");
|
||||
final Axis axis = switch (sAxis) {
|
||||
case "x" -> X;
|
||||
case "y" -> Y;
|
||||
case "z" -> Z;
|
||||
default -> throw new JsonParseException("Invalid axis " + sAxis);
|
||||
};
|
||||
final float angle = JsonUtil.loadFloat(json, "angle");
|
||||
final boolean rescale = JsonUtil.loadBool(json, "rescale", false);
|
||||
|
||||
return new ModelElement.Rotation(origin, axis, angle, rescale);
|
||||
} else {
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ModelElement.Face> loadFaces(JsonObject in) {
|
||||
|
||||
final List<ModelElement.Face> ret = new ObjectArrayList<>();
|
||||
final JsonObject json = in.getAsJsonObject("faces");
|
||||
|
||||
for (Map.Entry<String, JsonElement> e : json.entrySet()) {
|
||||
|
||||
final ForgeDirection side = fromName(e.getKey());
|
||||
final JsonObject face = e.getValue().getAsJsonObject();
|
||||
|
||||
final Vector4f uv = (face.has("uv")) ? loadVec4(face, "uv") : null;
|
||||
String texture = JsonUtil.loadStr(face, "texture");
|
||||
final ForgeDirection cullFace = fromName(JsonUtil.loadStr(face, "cullface", "unknown"));
|
||||
final int rotation = JsonUtil.loadInt(face, "rotation", 0);
|
||||
final int tintIndex = JsonUtil.loadInt(face, "tintindex", -1);
|
||||
|
||||
ret.add(new ModelElement.Face(side, uv, texture, cullFace, rotation, tintIndex));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static List<ModelElement> loadElements(JsonObject in) {
|
||||
|
||||
final List<ModelElement> ret = new ObjectArrayList<>();
|
||||
|
||||
if (in.has("elements")) {
|
||||
|
||||
final JsonArray arr = in.getAsJsonArray("elements");
|
||||
for (JsonElement e : arr) {
|
||||
|
||||
final JsonObject json = e.getAsJsonObject();
|
||||
final Vector3f from = loadVec3(json, "from").div(16);
|
||||
final Vector3f to = loadVec3(json, "to").div(16);
|
||||
final ModelElement.Rotation rotation = loadRotation(json);
|
||||
final boolean shade = JsonUtil.loadBool(json, "shade", true);
|
||||
final List<ModelElement.Face> faces = loadFaces(json);
|
||||
|
||||
ret.add(new ModelElement(from, to, rotation, shade, faces));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
|
||||
final JsonObject in = json.getAsJsonObject();
|
||||
|
||||
final String parent = JsonUtil.loadStr(in, "parent", null);
|
||||
ResourceLoc.ModelLoc parentId = null;
|
||||
if (parent != null && !parent.isEmpty()) {
|
||||
if (parent.contains(":")) {
|
||||
parentId = new ResourceLoc.ModelLoc(parent.split(":")[0], parent.split(":")[1]);
|
||||
} else {
|
||||
parentId = new ResourceLoc.ModelLoc("minecraft", parent);
|
||||
}
|
||||
}
|
||||
|
||||
final boolean useAO = JsonUtil.loadBool(in, "ambientocclusion", true);
|
||||
final Map<Position, ModelDisplay> display = loadDisplay(in);
|
||||
final var textures = loadTextures(in);
|
||||
final List<ModelElement> elements = loadElements(in);
|
||||
|
||||
return new JSONModel(parentId, useAO, display, textures, elements);
|
||||
}
|
||||
|
||||
public enum Position {
|
||||
|
||||
THIRDPERSON_RIGHTHAND,
|
||||
THIRDPERSON_LEFTHAND,
|
||||
FIRSTPERSON_RIGHTHAND,
|
||||
FIRSTPERSON_LEFTHAND,
|
||||
GUI, // inventory
|
||||
HEAD,
|
||||
GROUND, // dropped item I think
|
||||
FIXED; // item frames
|
||||
|
||||
public static Position getByName(String name) {
|
||||
return switch (name) {
|
||||
case "thirdperson_righthand" -> THIRDPERSON_RIGHTHAND;
|
||||
case "thirdperson_lefthand" -> THIRDPERSON_LEFTHAND;
|
||||
case "firstperson_righthand" -> FIRSTPERSON_RIGHTHAND;
|
||||
case "firstperson_lefthand" -> FIRSTPERSON_LEFTHAND;
|
||||
case "gui" -> GUI;
|
||||
case "head" -> HEAD;
|
||||
case "ground" -> GROUND;
|
||||
case "fixed" -> FIXED;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static final class ModelDisplay {
|
||||
|
||||
public static final ModelDisplay DEFAULT = new ModelDisplay(
|
||||
new Vector3f(0, 0, 0),
|
||||
new Vector3f(0, 0, 0),
|
||||
new Vector3f(1, 1, 1));
|
||||
private final Vector3f rotation;
|
||||
private final Vector3f translation;
|
||||
private final Vector3f scale;
|
||||
|
||||
public ModelDisplay(Vector3f rotation, Vector3f translation, Vector3f scale) {
|
||||
this.rotation = rotation;
|
||||
this.translation = translation;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public Vector3f rotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public Vector3f translation() {
|
||||
return translation;
|
||||
}
|
||||
|
||||
public Vector3f scale() {
|
||||
return scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ModelElement {
|
||||
|
||||
private final Vector3f from;
|
||||
private final Vector3f to;
|
||||
private final @Nullable Rotation rotation;
|
||||
private final boolean shade;
|
||||
private final List<Face> faces;
|
||||
|
||||
public ModelElement(Vector3f from, Vector3f to, @Nullable ModelDeserializer.ModelElement.Rotation rotation,
|
||||
boolean shade, List<Face> faces) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.rotation = rotation;
|
||||
this.shade = shade;
|
||||
this.faces = faces;
|
||||
}
|
||||
|
||||
public Vector3f from() {
|
||||
return from;
|
||||
}
|
||||
|
||||
public Vector3f to() {
|
||||
return to;
|
||||
}
|
||||
|
||||
public @Nullable Rotation rotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public boolean shade() {
|
||||
return shade;
|
||||
}
|
||||
|
||||
public List<Face> faces() {
|
||||
return faces;
|
||||
}
|
||||
|
||||
public static final class Face {
|
||||
|
||||
private final ForgeDirection name;
|
||||
private final Vector4f uv;
|
||||
private final String texture;
|
||||
private final ForgeDirection cullFace;
|
||||
private final int rotation;
|
||||
private final int tintIndex;
|
||||
|
||||
public Face(ForgeDirection name, Vector4f uv, String texture, ForgeDirection cullFace, int rotation,
|
||||
int tintIndex) {
|
||||
this.name = name;
|
||||
this.uv = uv;
|
||||
this.texture = texture;
|
||||
this.cullFace = cullFace;
|
||||
this.rotation = rotation;
|
||||
this.tintIndex = tintIndex;
|
||||
}
|
||||
|
||||
public ForgeDirection name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Vector4f uv() {
|
||||
return uv;
|
||||
}
|
||||
|
||||
public String texture() {
|
||||
return texture;
|
||||
}
|
||||
|
||||
public ForgeDirection cullFace() {
|
||||
return cullFace;
|
||||
}
|
||||
|
||||
public int rotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public int tintIndex() {
|
||||
return tintIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Rotation {
|
||||
|
||||
public static final Rotation NOOP = new Rotation(new Vector3f(0, 0, 0), X, 0, false);
|
||||
private final Vector3f origin;
|
||||
private final Axis axis;
|
||||
private final float angle;
|
||||
private final boolean rescale;
|
||||
|
||||
public Rotation(Vector3f origin, Axis axis, float angle, boolean rescale) {
|
||||
this.origin = origin;
|
||||
this.axis = axis;
|
||||
this.angle = (float) Math.toRadians(angle);
|
||||
this.rescale = rescale;
|
||||
}
|
||||
|
||||
public Matrix4f getAffineMatrix() {
|
||||
|
||||
// Subtract origin
|
||||
final Matrix4f ret = new Matrix4f().translation(-this.origin.x, -this.origin.y, -this.origin.z);
|
||||
|
||||
// Rotate
|
||||
switch (this.axis) {
|
||||
case X -> ret.rotateLocalX(angle);
|
||||
case Y -> ret.rotateLocalY(angle);
|
||||
case Z -> ret.rotateLocalZ(angle);
|
||||
}
|
||||
|
||||
// Add the origin back in
|
||||
return ret.translateLocal(this.origin.x, this.origin.y, this.origin.z);
|
||||
}
|
||||
|
||||
public Vector3f origin() {
|
||||
return origin;
|
||||
}
|
||||
|
||||
public Axis axis() {
|
||||
return axis;
|
||||
}
|
||||
|
||||
public float angle() {
|
||||
return angle;
|
||||
}
|
||||
|
||||
public boolean rescale() {
|
||||
return rescale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.loading;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.GTNHLibConfig.modelCacheSize;
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.unbaked.MissingModel.MISSING_MODEL;
|
||||
import static it.unimi.dsi.fastutil.objects.Object2ObjectMaps.unmodifiable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.client.resources.IResourceManager;
|
||||
import net.minecraft.client.resources.IResourceManagerReloadListener;
|
||||
import net.minecraft.client.resources.IResourcePack;
|
||||
import net.minecraftforge.client.event.TextureStitchEvent;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.baked.BakedModel;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ducks.BackingResourceManager;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ducks.GlobalResourceManager;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.BlockState;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.MissingState;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.StateDeserializer;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.StateModelMap;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.JSONModel;
|
||||
import com.gtnewhorizon.gtnhlib.concurrent.ThreadsafeCache;
|
||||
|
||||
import cpw.mods.fml.common.FMLContainerHolder;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectLists;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||
|
||||
/// Handles model loading and caching. All caches are size-based - this means that if a model has enough parents, it may
|
||||
/// exhaust the caches and unload itself before being fully baked. There *probably* won't be any consequences for this
|
||||
/// beyond excessively complex models being loaded multiple times... add a counter if I'm wrong.
|
||||
public class ModelRegistry {
|
||||
|
||||
public static final Logger MODEL_LOGGER = LogManager.getLogger(ModelRegistry.class);
|
||||
private static final Gson GSON = new GsonBuilder().registerTypeAdapter(StateModelMap.class, new StateDeserializer())
|
||||
.registerTypeAdapter(JSONModel.class, new ModelDeserializer()).create();
|
||||
|
||||
/// The first cache. Ideally, every request hits this and gets a baked model.
|
||||
private static final ThreadsafeCache<BlockState, BakedModel> BLOCKSTATE_MODEL_CACHE = new ThreadsafeCache<>(
|
||||
modelCacheSize,
|
||||
s -> bakeModel((BlockState) s),
|
||||
false);
|
||||
|
||||
/// If the first cache misses, we hit this to get the state map, so we can figure out which model to bake.
|
||||
private static final ThreadsafeCache<ResourceLoc.StateLoc, StateModelMap> STATE_MODEL_MAP_CACHE = new ThreadsafeCache<>(
|
||||
modelCacheSize,
|
||||
s -> ((ResourceLoc.StateLoc) s).load(() -> MissingState.MISSING_STATE_MAP, GSON),
|
||||
false);
|
||||
|
||||
/// {@link JSONModel}s may be shared across several {@link BakedModel}s, so we cache them too. The cache ensures all
|
||||
/// loaded models are resolved and ready to bake.
|
||||
///
|
||||
/// Note: the retriever was broken out into a function, because you can't read a field from within its own
|
||||
/// definition.
|
||||
private static final ThreadsafeCache<ResourceLoc.ModelLoc, JSONModel> JSON_MODEL_CACHE = new ThreadsafeCache<>(
|
||||
modelCacheSize,
|
||||
s -> loadAndResolveJSONModel((ResourceLoc.ModelLoc) s),
|
||||
false);
|
||||
|
||||
private static final String[] DEFAULT_STATE_KEYS = new String[] { "meta" };
|
||||
|
||||
private static BakedModel bakeModel(BlockState state) {
|
||||
final var block = state.block();
|
||||
final var meta = state.meta();
|
||||
|
||||
final var smm = getStateModelMap(block);
|
||||
final var properties = unmodifiable(
|
||||
new Object2ObjectArrayMap<String, String>(DEFAULT_STATE_KEYS, new String[] { Integer.toString(meta) }));
|
||||
|
||||
// Caching this would be a little pointless, since an UnbakedModel here would map directly to the BakedModel
|
||||
// missing from the cache... that's why we're loading one from scratch. The JSONModel *used* by the UnbakedModel
|
||||
// will be cached, however.
|
||||
final var dough = smm.selectModel(properties);
|
||||
if (dough == null) return MISSING_MODEL.bake();
|
||||
|
||||
return dough.bake();
|
||||
}
|
||||
|
||||
/// Getter for {@link BakedModel}s. We don't want to publicly expose the cache, modders can't be trusted with it :P
|
||||
public static BakedModel getBakedModel(BlockState state) {
|
||||
return BLOCKSTATE_MODEL_CACHE.get(state);
|
||||
}
|
||||
|
||||
/// Getter for {@link JSONModel}s. See {@link ModelRegistry#getBakedModel(BlockState)}
|
||||
public static JSONModel getJSONModel(ResourceLoc.ModelLoc loc) {
|
||||
return JSON_MODEL_CACHE.get(loc);
|
||||
}
|
||||
|
||||
/// Registers the given mod ID for automatic texture loading. The resource pack attached to this mod will be scanned
|
||||
/// for model files, and textures from those files will be automatically loaded.
|
||||
public static void registerModid(String modid) {
|
||||
ReloadListener.PERMITTED_MODIDS.add(modid);
|
||||
}
|
||||
|
||||
private static StateModelMap getStateModelMap(Block block) {
|
||||
final var name = BlockName.fromBlock(block);
|
||||
final var stateLocation = new ResourceLoc.StateLoc(name.domain, name.name);
|
||||
return STATE_MODEL_MAP_CACHE.get(stateLocation);
|
||||
}
|
||||
|
||||
private static JSONModel loadAndResolveJSONModel(ResourceLoc.ModelLoc loc) {
|
||||
final var m = loc.load(() -> MISSING_MODEL, GSON);
|
||||
m.resolveParents(JSON_MODEL_CACHE::get);
|
||||
return m;
|
||||
}
|
||||
|
||||
private static final class BlockName {
|
||||
|
||||
private final String domain;
|
||||
private final String name;
|
||||
|
||||
private BlockName(String domain, String name) {
|
||||
this.domain = domain;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
private static BlockName fromBlock(Block block) {
|
||||
final String blockName = Block.blockRegistry.getNameForObject(block);
|
||||
|
||||
final int sepIdx = blockName.indexOf(":");
|
||||
final var domain = blockName.substring(0, sepIdx);
|
||||
final var name = blockName.substring(sepIdx + 1);
|
||||
|
||||
return new BlockName(domain, name);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReloadListener implements IResourceManagerReloadListener {
|
||||
|
||||
private static final ObjectOpenHashSet<String> PERMITTED_MODIDS = new ObjectOpenHashSet<>();
|
||||
|
||||
@Override
|
||||
public void onResourceManagerReload(IResourceManager irm) {
|
||||
// Flush all caches, we may have new models
|
||||
BLOCKSTATE_MODEL_CACHE.clear();
|
||||
STATE_MODEL_MAP_CACHE.clear();
|
||||
JSON_MODEL_CACHE.clear();
|
||||
|
||||
if (!(irm instanceof GlobalResourceManager manager)) return;
|
||||
|
||||
final var domains = manager.nhlib$getDomainResourceManagers();
|
||||
final var resourcePacks = new ObjectOpenHashSet<IResourcePack>();
|
||||
for (var domain : domains.entrySet()) {
|
||||
final var files = domain.getValue();
|
||||
resourcePacks.addAll(((BackingResourceManager) files).nhlib$getResourcePacks());
|
||||
}
|
||||
|
||||
final var texturesToLoad = new ObjectArrayList<String>();
|
||||
for (var pack : resourcePacks) {
|
||||
if (!(pack instanceof ModelResourcePack mrp)) continue;
|
||||
|
||||
// Skip unregistered mods
|
||||
if (mrp instanceof FMLContainerHolder fmlch
|
||||
&& !PERMITTED_MODIDS.contains(fmlch.getFMLContainer().getModId()))
|
||||
continue;
|
||||
|
||||
final var texs = mrp.nhlib$getReferencedTextures(reader -> GSON.fromJson(reader, JSONModel.class));
|
||||
texturesToLoad.addAll(texs);
|
||||
}
|
||||
|
||||
EventHandler.texturesToLoad = texturesToLoad;
|
||||
}
|
||||
}
|
||||
|
||||
public static class EventHandler {
|
||||
|
||||
private static List<String> texturesToLoad = ObjectLists.emptyList();
|
||||
|
||||
@SubscribeEvent
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onTextureStitch(TextureStitchEvent.Pre event) {
|
||||
for (var tex : texturesToLoad) event.map.registerIcon(tex.replaceFirst("^minecraft:", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.loading;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.JSONModel;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectLists;
|
||||
|
||||
public interface ModelResourcePack {
|
||||
|
||||
/// Returns a list of the texture names referenced by models in this resource pack.
|
||||
default List<String> nhlib$getReferencedTextures(Function<Reader, JSONModel> jsonParser) {
|
||||
return ObjectLists.emptyList();
|
||||
}
|
||||
|
||||
default List<String> nhlib$getReferencedTextures(Stream<? extends InputStream> files,
|
||||
Function<Reader, JSONModel> jsonParser) {
|
||||
return files.flatMap(is -> {
|
||||
final var model = jsonParser.apply(new InputStreamReader(is));
|
||||
return model.getTextures().values().stream().filter(v -> {
|
||||
// Ignore texture variables
|
||||
return !v.startsWith("#");
|
||||
});
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.loading;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry.MODEL_LOGGER;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.util.JsonException;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.StateModelMap;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.JSONModel;
|
||||
|
||||
public interface ResourceLoc<T> {
|
||||
|
||||
default T load(Supplier<@NotNull T> defaultSrc, Gson gson) {
|
||||
final var jsonPath = new ResourceLocation(owner(), prefix() + path() + ext());
|
||||
|
||||
try {
|
||||
final InputStream is = Minecraft.getMinecraft().getResourceManager().getResource(jsonPath).getInputStream();
|
||||
return gson.fromJson(new InputStreamReader(is), clazz());
|
||||
} catch (JsonException e) {
|
||||
MODEL_LOGGER.error("Failed to parse {}:{}", owner(), jsonPath.getResourcePath());
|
||||
} catch (IOException e) {
|
||||
MODEL_LOGGER.error("Could not load {}:{}", owner(), jsonPath.getResourcePath());
|
||||
}
|
||||
return defaultSrc.get();
|
||||
}
|
||||
|
||||
String prefix();
|
||||
|
||||
String owner();
|
||||
|
||||
String path();
|
||||
|
||||
String ext();
|
||||
|
||||
Class<T> clazz();
|
||||
|
||||
final class ModelLoc implements ResourceLoc<JSONModel> {
|
||||
|
||||
private final String owner;
|
||||
private final String path;
|
||||
|
||||
public ModelLoc(String owner, String path) {
|
||||
this.owner = owner;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public static ModelLoc fromStr(String id) {
|
||||
final int sepIdx = id.indexOf(':');
|
||||
if (sepIdx < 0) return new ModelLoc("minecraft", id);
|
||||
return new ModelLoc(id.substring(0, sepIdx), id.substring(sepIdx + 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String prefix() {
|
||||
return "models/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String ext() {
|
||||
return ".json";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<JSONModel> clazz() {
|
||||
return JSONModel.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String owner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (ModelLoc) obj;
|
||||
return Objects.equals(this.owner, that.owner) && Objects.equals(this.path, that.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(owner, path);
|
||||
}
|
||||
}
|
||||
|
||||
final class StateLoc implements ResourceLoc<StateModelMap> {
|
||||
|
||||
private final String owner;
|
||||
private final String path;
|
||||
|
||||
public StateLoc(String owner, String path) {
|
||||
this.owner = owner;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String prefix() {
|
||||
return "blockstates/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String ext() {
|
||||
return ".json";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<StateModelMap> clazz() {
|
||||
return StateModelMap.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String owner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (StateLoc) obj;
|
||||
return Objects.equals(this.owner, that.owner) && Objects.equals(this.path, that.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(owner, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.loading.ducks;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.client.resources.IResourcePack;
|
||||
|
||||
public interface BackingResourceManager {
|
||||
|
||||
List<IResourcePack> nhlib$getResourcePacks();
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.loading.ducks;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import net.minecraft.client.resources.FallbackResourceManager;
|
||||
|
||||
public interface GlobalResourceManager {
|
||||
|
||||
Map<String, FallbackResourceManager> nhlib$getDomainResourceManagers();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.state;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
|
||||
public final class BlockState {
|
||||
|
||||
private final Block block;
|
||||
private final int meta;
|
||||
|
||||
public BlockState(Block block, int meta) {
|
||||
this.block = block;
|
||||
this.meta = meta;
|
||||
}
|
||||
|
||||
public Block block() {
|
||||
return block;
|
||||
}
|
||||
|
||||
public int meta() {
|
||||
return meta;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (BlockState) obj;
|
||||
return Objects.equals(this.block, that.block) && this.meta == that.meta;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(block, meta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.state;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.UnbakedModel;
|
||||
|
||||
public class MissingState implements StateModelMap {
|
||||
|
||||
public static final MissingState MISSING_STATE_MAP = new MissingState();
|
||||
|
||||
@Override
|
||||
public @Nullable UnbakedModel selectModel(Map<String, String> properties) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.state;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.JSONVariant;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.Weighted;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.MonopartDough;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.UnbakedModel;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
|
||||
public class MonopartState implements StateModelMap {
|
||||
|
||||
private final Object2ObjectMap<StateMatch, ObjectList<Weighted<JSONVariant>>> variants;
|
||||
|
||||
MonopartState(Object2ObjectMap<StateMatch, ObjectList<Weighted<JSONVariant>>> variants) {
|
||||
this.variants = Object2ObjectMaps.unmodifiable(variants);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable UnbakedModel selectModel(Map<String, String> properties) {
|
||||
final var iter = Object2ObjectMaps.fastIterator(variants);
|
||||
while (iter.hasNext()) {
|
||||
final var e = iter.next();
|
||||
final var match = e.getKey();
|
||||
|
||||
if (match.matches(properties)) return new MonopartDough(e.getValue());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static class StateMatch {
|
||||
|
||||
private final boolean matchAll;
|
||||
private final Object2ObjectMap<String, String> states;
|
||||
|
||||
StateMatch(String s) {
|
||||
if (s.isEmpty()) {
|
||||
matchAll = true;
|
||||
states = null;
|
||||
return;
|
||||
}
|
||||
matchAll = false;
|
||||
states = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
final var cases = s.split(",");
|
||||
for (var c : cases) {
|
||||
final int eidx = c.indexOf("=");
|
||||
if (eidx == -1) throw new RuntimeException("Unexpected blockstate case '" + c + "'!");
|
||||
|
||||
states.put(c.substring(0, eidx), c.substring(eidx + 1));
|
||||
}
|
||||
}
|
||||
|
||||
boolean matches(Map<String, String> properties) {
|
||||
if (matchAll) return true;
|
||||
|
||||
final var iter = Object2ObjectMaps.fastIterator(states);
|
||||
while (iter.hasNext()) {
|
||||
final var e = iter.next();
|
||||
if (!Objects.equals(properties.get(e.getKey()), e.getValue())) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.state;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.BakeData;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.JSONVariant;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.Weighted;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.baked.BakedModel;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.baked.MultipartModel;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.MultipartState.Case.Condition;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.MonopartDough;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.UnbakedModel;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
|
||||
public class MultipartState implements StateModelMap, UnbakedModel {
|
||||
|
||||
private final ObjectList<Case> multipart;
|
||||
|
||||
MultipartState(ObjectList<Case> multipart) {
|
||||
this.multipart = multipart;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UnbakedModel selectModel(Map<String, String> properties) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BakedModel bake(BakeData data) {
|
||||
final var bread = new Object2ObjectArrayMap<Condition, BakedModel>(multipart.size());
|
||||
for (var c : multipart) {
|
||||
bread.put(c.when, new MonopartDough(c.apply).bake());
|
||||
}
|
||||
|
||||
return new MultipartModel(bread);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public static final class Case {
|
||||
|
||||
private final ObjectList<Weighted<JSONVariant>> apply;
|
||||
private final Condition when;
|
||||
|
||||
public Case(ObjectList<Weighted<JSONVariant>> apply, Condition when) {
|
||||
this.apply = apply;
|
||||
this.when = when;
|
||||
}
|
||||
|
||||
public ObjectList<Weighted<JSONVariant>> apply() {
|
||||
return apply;
|
||||
}
|
||||
|
||||
public Condition when() {
|
||||
return when;
|
||||
}
|
||||
|
||||
public interface Condition {
|
||||
|
||||
Condition TRUE = (Map<String, String> state) -> true;
|
||||
|
||||
boolean matches(Map<String, String> state);
|
||||
}
|
||||
|
||||
static final class MultiCon implements Condition {
|
||||
|
||||
private final boolean requireAll;
|
||||
private final ObjectList<Condition> matches;
|
||||
|
||||
MultiCon(boolean requireAll, ObjectList<Condition> matches) {
|
||||
this.requireAll = requireAll;
|
||||
this.matches = matches;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Map<String, String> state) {
|
||||
if (requireAll) {
|
||||
for (var m : matches) {
|
||||
if (!m.matches(state)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var m : matches) {
|
||||
if (m.matches(state)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean requireAll() {
|
||||
return requireAll;
|
||||
}
|
||||
|
||||
public ObjectList<Condition> matches() {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
static final class StateCon implements Condition {
|
||||
|
||||
private final String key;
|
||||
private final ObjectList<String> values;
|
||||
|
||||
StateCon(String key, ObjectList<String> values) {
|
||||
this.key = key;
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Map<String, String> state) {
|
||||
final var val = state.get(key);
|
||||
for (var v : values) {
|
||||
if (v.equals(val)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public ObjectList<String> values() {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.state;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.JSONVariant.weightedVariant;
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.loading.ResourceLoc.ModelLoc.fromStr;
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.state.MonopartState.StateMatch;
|
||||
import static com.gtnewhorizon.gtnhlib.util.JsonUtil.loadBool;
|
||||
import static com.gtnewhorizon.gtnhlib.util.JsonUtil.loadInt;
|
||||
import static com.gtnewhorizon.gtnhlib.util.JsonUtil.loadStr;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.JSONVariant;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.Weighted;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.MultipartState.Case;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.MultipartState.Case.Condition;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.MultipartState.Case.MultiCon;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.state.MultipartState.Case.StateCon;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
|
||||
public class StateDeserializer implements JsonDeserializer<StateModelMap> {
|
||||
|
||||
@Override
|
||||
public StateModelMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
final var root = json.getAsJsonObject();
|
||||
|
||||
if (root.has("variants")) {
|
||||
return new MonopartState(loadVariants(root));
|
||||
}
|
||||
|
||||
if (root.has("multipart")) {
|
||||
return new MultipartState(loadMultipart(root));
|
||||
}
|
||||
|
||||
throw new JsonParseException("No 'variants' or 'multipart' tag found in blockstate JSON!");
|
||||
}
|
||||
|
||||
private Object2ObjectMap<StateMatch, ObjectList<Weighted<JSONVariant>>> loadVariants(JsonObject root) {
|
||||
final var variants = root.getAsJsonObject("variants");
|
||||
|
||||
var entries = variants.entrySet();
|
||||
var map = new Object2ObjectOpenHashMap<StateMatch, ObjectList<Weighted<JSONVariant>>>(entries.size());
|
||||
|
||||
entries.forEach(v -> {
|
||||
var match = v.getKey();
|
||||
var variant = v.getValue();
|
||||
map.put(new StateMatch(match), loadVariants(variant));
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private ObjectList<Case> loadMultipart(JsonObject root) {
|
||||
final var multipart = root.getAsJsonArray("multipart");
|
||||
final var cases = new ObjectArrayList<Case>(multipart.size());
|
||||
|
||||
for (var c : multipart) {
|
||||
final var kase = c.getAsJsonObject();
|
||||
final var variants = loadVariants(kase.get("apply"));
|
||||
final var condition = loadCondition(kase);
|
||||
cases.add(new Case(variants, condition));
|
||||
}
|
||||
|
||||
return cases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a list of jsonVariants from the given element. If it's an array, the list may contain multiple - otherwise
|
||||
* the list wil contain one.
|
||||
*/
|
||||
private ObjectList<Weighted<JSONVariant>> loadVariants(JsonElement variants) {
|
||||
final ObjectList<Weighted<JSONVariant>> loadedJSONVariants;
|
||||
if (variants.isJsonArray()) {
|
||||
var models = variants.getAsJsonArray();
|
||||
loadedJSONVariants = new ObjectArrayList<>(models.size());
|
||||
|
||||
for (var m : models) {
|
||||
var model = m.getAsJsonObject();
|
||||
loadedJSONVariants.add(loadVariant(model));
|
||||
}
|
||||
} else {
|
||||
var model = variants.getAsJsonObject();
|
||||
loadedJSONVariants = new ObjectArrayList<>(1);
|
||||
loadedJSONVariants.add(loadVariant(model));
|
||||
}
|
||||
|
||||
return loadedJSONVariants;
|
||||
}
|
||||
|
||||
private Weighted<JSONVariant> loadVariant(JsonObject variant) {
|
||||
return weightedVariant(
|
||||
fromStr(loadStr(variant, "model")),
|
||||
loadInt(variant, "x", 0),
|
||||
loadInt(variant, "y", 0),
|
||||
loadBool(variant, "uvlock", false),
|
||||
loadInt(variant, "weight", 1));
|
||||
}
|
||||
|
||||
private Condition loadCondition(JsonObject kase) {
|
||||
if (!kase.has("when")) {
|
||||
return Condition.TRUE;
|
||||
}
|
||||
|
||||
final var when = kase.getAsJsonObject("when");
|
||||
if (when.has("AND")) {
|
||||
final var cons = when.getAsJsonArray("AND");
|
||||
final var conditions = LoadMultiCons(cons);
|
||||
return new MultiCon(true, conditions);
|
||||
} else if (when.has("OR")) {
|
||||
final var cons = when.getAsJsonArray("OR");
|
||||
final var conditions = LoadMultiCons(cons);
|
||||
return new MultiCon(false, conditions);
|
||||
}
|
||||
|
||||
return new MultiCon(true, loadStateCons(when));
|
||||
}
|
||||
|
||||
/// Given a segment like ```
|
||||
/// [
|
||||
/// {
|
||||
/// "facing": "east",
|
||||
/// "beans": "toast"
|
||||
/// },
|
||||
/// {
|
||||
/// "pied": "false"
|
||||
/// }
|
||||
/// ]
|
||||
/// ```
|
||||
/// returns a list of the MultiCons `[facing = east && beans = toast], [pied = false]`
|
||||
private ObjectArrayList<Condition> LoadMultiCons(JsonArray cons) {
|
||||
final var conditions = new ObjectArrayList<Condition>(cons.size());
|
||||
|
||||
for (var con : cons) {
|
||||
conditions.add(new MultiCon(true, loadStateCons(con.getAsJsonObject())));
|
||||
}
|
||||
return conditions;
|
||||
}
|
||||
|
||||
/// Given a segment like ```json
|
||||
/// {
|
||||
/// "facing": "east",
|
||||
/// "beans": "toast"
|
||||
/// },```
|
||||
/// returns a list of StateCon `[facing = east, beans = toast]`
|
||||
private static @NotNull ObjectArrayList<Condition> loadStateCons(JsonObject obj) {
|
||||
final var conds = obj.entrySet();
|
||||
final var condList = new ObjectArrayList<Condition>(conds.size());
|
||||
|
||||
for (var cond : conds) {
|
||||
final var stateName = cond.getKey();
|
||||
final var stateValues = new ObjectArrayList<>(cond.getValue().getAsString().split("\\|"));
|
||||
|
||||
condList.add(new StateCon(stateName, stateValues));
|
||||
}
|
||||
return condList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.state;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.UnbakedModel;
|
||||
|
||||
public interface StateModelMap {
|
||||
|
||||
/**
|
||||
* @param properties The blockstate properties as strings
|
||||
* @return The appropriate model, or null if none match
|
||||
*/
|
||||
@Nullable
|
||||
UnbakedModel selectModel(Map<String, String> properties);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.template;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.IBlockPos;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Quad;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadProvider;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
|
||||
|
||||
/**
|
||||
* While this class does not provide dynamic quads, they still need to be threadlocaled because the color of the quads
|
||||
* are modified.
|
||||
*/
|
||||
public class BlockColoredCube extends BlockStaticCube {
|
||||
|
||||
private ThreadLocal<List<QuadView>[]> threadedStore;
|
||||
|
||||
public BlockColoredCube(String block) {
|
||||
super(block);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bake() {
|
||||
super.bake();
|
||||
|
||||
// Kind of a mess, but it's required because I don't want to just store quads - that would require initializing
|
||||
// a new List every time. This ain't efficient, but it only runs when a new thread is spun off.
|
||||
this.threadedStore = ThreadLocal.withInitial(() -> {
|
||||
final List<QuadView>[] ret = new List[7];
|
||||
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
ret[i] = this.store[i].stream().map(new Quad()::copyFrom)
|
||||
.collect(ObjectImmutableList.toListWithExpectedSize(1));
|
||||
}
|
||||
ret[6] = ObjectImmutableList.of();
|
||||
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor(IBlockAccess world, IBlockPos pos, Block block, int meta, Random random) {
|
||||
return QuadProvider.getDefaultColor(world, pos, block);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuadView> getQuads(IBlockAccess world, IBlockPos pos, Block block, int meta, ForgeDirection dir,
|
||||
Random random, int color, Supplier<QuadView> quadPool) {
|
||||
|
||||
final List<QuadView> ret = this.threadedStore.get()[dir.ordinal()];
|
||||
if (dir != ForgeDirection.UNKNOWN) ret.get(0).setColors(color);
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.template;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.IBlockPos;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.ModelLoader;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.NdQuadBuilder;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Quad;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadBuilder;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadProvider;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
|
||||
|
||||
public class BlockStaticCube implements QuadProvider {
|
||||
|
||||
private final String textureName;
|
||||
protected final List<QuadView>[] store = new List[7];
|
||||
|
||||
public BlockStaticCube(String textureName) {
|
||||
this.textureName = textureName;
|
||||
ModelLoader.registerBaker(this::bake);
|
||||
}
|
||||
|
||||
protected void bake() {
|
||||
|
||||
final NdQuadBuilder builder = new NdQuadBuilder();
|
||||
for (ForgeDirection f : ForgeDirection.VALID_DIRECTIONS) {
|
||||
|
||||
builder.square(f, 0, 0, 1, 1, 0);
|
||||
builder.spriteBake(this.textureName, QuadBuilder.BAKE_LOCK_UV);
|
||||
builder.setColors(-1);
|
||||
this.store[f.ordinal()] = ObjectImmutableList.of(builder.build(new Quad()));
|
||||
}
|
||||
this.store[6] = ObjectImmutableList.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuadView> getQuads(IBlockAccess world, IBlockPos pos, Block block, int meta, ForgeDirection dir,
|
||||
Random random, int color, Supplier<QuadView> quadPool) {
|
||||
return store[dir.ordinal()];
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.template;
|
||||
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.ModelLoader;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.Variant;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadProvider;
|
||||
|
||||
/**
|
||||
* Use this to create a full cube column rotatable in 3 directions - vertical, x, and z
|
||||
*/
|
||||
public class Column3Rot {
|
||||
|
||||
public final Variant[] variants = new Variant[3];
|
||||
public final QuadProvider[] models = new QuadProvider[3];
|
||||
|
||||
public Column3Rot(ResourceLocation model) {
|
||||
|
||||
this.variants[0] = new Variant(model, 0, 0, 0, false);
|
||||
this.variants[1] = new Variant(model, 90, 90, 0, false);
|
||||
this.variants[2] = new Variant(model, 90, 0, 0, false);
|
||||
|
||||
ModelLoader.registerModels(
|
||||
() -> { for (int i = 0; i < 3; ++i) this.models[i] = ModelLoader.getModel(this.variants[i]); },
|
||||
this.variants);
|
||||
}
|
||||
|
||||
public QuadProvider updown() {
|
||||
return this.models[0];
|
||||
}
|
||||
|
||||
public QuadProvider eastwest() {
|
||||
return this.models[1];
|
||||
}
|
||||
|
||||
public QuadProvider northsouth() {
|
||||
return this.models[2];
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.template;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.IBlockPos;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.ModelLoader;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.NdQuadBuilder;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Quad;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadBuilder;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadProvider;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
|
||||
|
||||
public class ColumnModel implements QuadProvider {
|
||||
|
||||
private final String topTex;
|
||||
private final String sideTex;
|
||||
protected final List<QuadView>[] store = new List[7];
|
||||
private boolean rotate = false;
|
||||
private Matrix4f rotMat;
|
||||
|
||||
public ColumnModel(String topTex, String sideTex, Matrix4f rotMat) {
|
||||
this(topTex, sideTex);
|
||||
this.rotate = true;
|
||||
this.rotMat = rotMat;
|
||||
}
|
||||
|
||||
public ColumnModel(String topTex, String sideTex) {
|
||||
this.topTex = topTex;
|
||||
this.sideTex = sideTex;
|
||||
ModelLoader.registerBaker(this::bake);
|
||||
}
|
||||
|
||||
protected void bake() {
|
||||
|
||||
final NdQuadBuilder builder = new NdQuadBuilder();
|
||||
|
||||
for (ForgeDirection f : ForgeDirection.VALID_DIRECTIONS) {
|
||||
|
||||
builder.square(f, 0, 0, 1, 1, 0);
|
||||
|
||||
final String tex = (f == ForgeDirection.UP || f == ForgeDirection.DOWN) ? this.topTex : this.sideTex;
|
||||
builder.spriteBake(tex, QuadBuilder.BAKE_LOCK_UV);
|
||||
|
||||
builder.setColors(-1);
|
||||
|
||||
final List<QuadView> tmp = ObjectImmutableList
|
||||
.of((this.rotate) ? builder.build(new Quad(), this.rotMat) : builder.build(new Quad()));
|
||||
|
||||
this.store[tmp.get(0).getCullFace().ordinal()] = tmp;
|
||||
}
|
||||
this.store[6] = ObjectImmutableList.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuadView> getQuads(IBlockAccess world, IBlockPos pos, Block block, int meta, ForgeDirection dir,
|
||||
Random random, int color, Supplier<QuadView> quadPool) {
|
||||
return this.store[dir.ordinal()];
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.template;
|
||||
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.ModelLoader;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.Variant;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadProvider;
|
||||
|
||||
/**
|
||||
* Use this to create JSON model rotatable in 4 directions - NSWE
|
||||
*/
|
||||
public class Model4Rot {
|
||||
|
||||
public final QuadProvider[] models = new QuadProvider[4];
|
||||
private final Variant[] modelIds;
|
||||
|
||||
public Model4Rot(ResourceLocation modelLoc) {
|
||||
|
||||
this.modelIds = new Variant[] { new Variant(modelLoc, 0, 0, 0, false), new Variant(modelLoc, 0, 180, 0, false),
|
||||
new Variant(modelLoc, 0, 90, 0, false), new Variant(modelLoc, 0, 270, 0, false) };
|
||||
|
||||
ModelLoader.registerModels(() -> loadModels(this), this.modelIds);
|
||||
}
|
||||
|
||||
public static void loadModels(Model4Rot model) {
|
||||
for (int i = 0; i < 4; ++i) model.models[i] = ModelLoader.getModel(model.modelIds[i]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.unbaked;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.loading.ModelDeserializer.ModelElement.Rotation.NOOP;
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry.MODEL_LOGGER;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.POS_Y;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.UNASSIGNED;
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
import static org.joml.Math.fma;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.BakeData;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.baked.BakedModel;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.baked.PileOfQuads;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelDeserializer;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelDeserializer.ModelElement.Face;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelDeserializer.Position;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelDeserializer.Position.ModelDisplay;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ResourceLoc.ModelLoc;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuad;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
|
||||
public class JSONModel implements UnbakedModel {
|
||||
|
||||
@Nullable
|
||||
private final ModelLoc parentId;
|
||||
@Nullable
|
||||
private JSONModel parent;
|
||||
private final boolean useAO;
|
||||
private final Map<Position, ModelDisplay> display;
|
||||
@NotNull
|
||||
private final Object2ObjectMap<String, String> textures;
|
||||
private List<ModelDeserializer.ModelElement> elements;
|
||||
|
||||
private static final Vector4f DEFAULT_UV = new Vector4f(0, 0, 16, 16);
|
||||
|
||||
public JSONModel(@Nullable ModelLoc parentId, boolean useAO, Map<Position, ModelDisplay> display,
|
||||
@NotNull Object2ObjectMap<String, String> textures, List<ModelDeserializer.ModelElement> elements) {
|
||||
this.parentId = parentId;
|
||||
this.useAO = useAO;
|
||||
this.display = display;
|
||||
this.textures = textures;
|
||||
this.elements = elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a shallow copy of og. This allows you to bake the same model multiple times with various transformations.
|
||||
*/
|
||||
public JSONModel(JSONModel og) {
|
||||
|
||||
this.parentId = og.parentId;
|
||||
this.parent = og.parent;
|
||||
this.useAO = og.useAO;
|
||||
this.display = og.display;
|
||||
this.textures = og.textures;
|
||||
this.elements = og.elements;
|
||||
}
|
||||
|
||||
private static void setUV(ModelQuadViewMutable q, int i, float u, float v) {
|
||||
q.setTexU(i, u);
|
||||
q.setTexV(i, v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modern Minecraft uses magic arrays to do this without breaking AO. This is the same thing, but without arrays.
|
||||
* Note: still doesn't fix AO. Whoops.
|
||||
*/
|
||||
@NotNull
|
||||
private static Vector3f mapSideToVertex(Vector3f from, Vector3f to, int index, ForgeDirection side) {
|
||||
return switch (side) {
|
||||
case DOWN -> switch (index) {
|
||||
case 0 -> new Vector3f(from.x, from.y, to.z);
|
||||
case 1 -> new Vector3f(from.x, from.y, from.z);
|
||||
case 2 -> new Vector3f(to.x, from.y, from.z);
|
||||
case 3 -> new Vector3f(to.x, from.y, to.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case UP -> switch (index) {
|
||||
case 0 -> new Vector3f(from.x, to.y, from.z);
|
||||
case 1 -> new Vector3f(from.x, to.y, to.z);
|
||||
case 2 -> new Vector3f(to.x, to.y, to.z);
|
||||
case 3 -> new Vector3f(to.x, to.y, from.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case NORTH -> switch (index) {
|
||||
case 0 -> new Vector3f(to.x, to.y, from.z);
|
||||
case 1 -> new Vector3f(to.x, from.y, from.z);
|
||||
case 2 -> new Vector3f(from.x, from.y, from.z);
|
||||
case 3 -> new Vector3f(from.x, to.y, from.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case SOUTH -> switch (index) {
|
||||
case 0 -> new Vector3f(from.x, to.y, to.z);
|
||||
case 1 -> new Vector3f(from.x, from.y, to.z);
|
||||
case 2 -> new Vector3f(to.x, from.y, to.z);
|
||||
case 3 -> new Vector3f(to.x, to.y, to.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case WEST -> switch (index) {
|
||||
case 0 -> new Vector3f(from.x, to.y, from.z);
|
||||
case 1 -> new Vector3f(from.x, from.y, from.z);
|
||||
case 2 -> new Vector3f(from.x, from.y, to.z);
|
||||
case 3 -> new Vector3f(from.x, to.y, to.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case EAST -> switch (index) {
|
||||
case 0 -> new Vector3f(to.x, to.y, to.z);
|
||||
case 1 -> new Vector3f(to.x, from.y, to.z);
|
||||
case 2 -> new Vector3f(to.x, from.y, from.z);
|
||||
case 3 -> new Vector3f(to.x, to.y, from.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case UNKNOWN -> throw new IllegalArgumentException("No vector matching UNKNOWN!");
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public BakedModel bake(BakeData data) {
|
||||
|
||||
final var vRot = data.getAffineMatrix();
|
||||
final var sidedQuadStore = new HashMap<ModelQuadFacing, ArrayList<ModelQuadView>>(7);
|
||||
|
||||
// Append faces from each element
|
||||
for (ModelDeserializer.ModelElement e : this.elements) {
|
||||
|
||||
final Matrix4f rot = (e.rotation() == null) ? NOOP.getAffineMatrix() : e.rotation().getAffineMatrix();
|
||||
|
||||
final Vector3f from = e.from();
|
||||
final Vector3f to = e.to();
|
||||
|
||||
for (Face f : e.faces()) {
|
||||
|
||||
float x = Float.MAX_VALUE;
|
||||
float y = Float.MAX_VALUE;
|
||||
float z = Float.MAX_VALUE;
|
||||
float X = Float.MIN_VALUE;
|
||||
float Y = Float.MIN_VALUE;
|
||||
float Z = Float.MIN_VALUE;
|
||||
|
||||
// Assign vertexes
|
||||
final var quad = new ModelQuad();
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
|
||||
final Vector3f vert = mapSideToVertex(from, to, i, f.name()).mulPosition(rot).mulPosition(vRot);
|
||||
quad.setX(i, vert.x);
|
||||
quad.setY(i, vert.y);
|
||||
quad.setZ(i, vert.z);
|
||||
|
||||
x = min(x, vert.x);
|
||||
y = min(y, vert.y);
|
||||
z = min(z, vert.z);
|
||||
X = max(X, vert.x);
|
||||
Y = max(Y, vert.y);
|
||||
Z = max(Z, vert.z);
|
||||
}
|
||||
|
||||
// Set culling and nominal faces
|
||||
final var normFace = quad.getNormalFace();
|
||||
quad.setLightFace(normFace != UNASSIGNED ? normFace : POS_Y);
|
||||
|
||||
// Set UV
|
||||
// TODO: UV locking
|
||||
final Vector4f uv = Objects.firstNonNull(f.uv(), DEFAULT_UV);
|
||||
setUV(quad, 0, uv.x, uv.y);
|
||||
setUV(quad, 1, uv.x, uv.w);
|
||||
setUV(quad, 2, uv.z, uv.w);
|
||||
setUV(quad, 3, uv.z, uv.y);
|
||||
|
||||
// Set the sprite
|
||||
var texKey = f.texture();
|
||||
var texName = textures.get(texKey);
|
||||
if (texName.startsWith("#")) {
|
||||
MODEL_LOGGER.warn("Model {} has unflattened texture variable {} when baking!", this, texName);
|
||||
textures.put(texKey, "minecraft:missing");
|
||||
texName = "minecraft:missing";
|
||||
}
|
||||
bakeSprite(quad, texName);
|
||||
|
||||
// Set the tint index
|
||||
quad.setColorIndex(f.tintIndex());
|
||||
|
||||
// Set AO
|
||||
quad.setHasAmbientOcclusion(this.useAO);
|
||||
|
||||
// Bake and add it
|
||||
sidedQuadStore.computeIfAbsent(quad.getNormalFace(), d -> new ArrayList<>()).add(quad);
|
||||
}
|
||||
}
|
||||
|
||||
// Add them to the model
|
||||
return new PileOfQuads(sidedQuadStore);
|
||||
}
|
||||
|
||||
// TODO fix
|
||||
private void bakeSprite(ModelQuadViewMutable quad, String name) {
|
||||
name = name.replaceFirst("^minecraft:", "");
|
||||
final var icon = Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(name);
|
||||
final float minU = icon.getMinU();
|
||||
final float minV = icon.getMinV();
|
||||
final float dU = icon.getMaxU() - minU;
|
||||
final float dV = icon.getMaxV() - minV;
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
quad.setTexU(i, fma(dU, quad.getTexU(i) / 16, minU));
|
||||
quad.setTexV(i, fma(dV, quad.getTexV(i) / 16, minV));
|
||||
}
|
||||
}
|
||||
|
||||
public void resolveParents(Function<ModelLoc, JSONModel> modelLoader) {
|
||||
|
||||
if (this.parentId == null || this.parent != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final JSONModel p = modelLoader.apply(this.parentId);
|
||||
p.resolveParents(modelLoader);
|
||||
|
||||
// Inherit properties
|
||||
this.parent = p;
|
||||
if (this.elements.isEmpty()) this.elements = this.parent.elements;
|
||||
|
||||
// Resolve texture variables
|
||||
// Add parent texture mappings, but prioritize ours.
|
||||
for (Map.Entry<String, String> e : this.parent.textures.entrySet()) {
|
||||
|
||||
this.textures.putIfAbsent(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
// Flatten them, merging s -> s1, s1 -> s2 to s -> s2, s1 -> s2.
|
||||
boolean flat = false;
|
||||
final Map<String, String> tmp = new Object2ObjectOpenHashMap<>();
|
||||
while (!flat) {
|
||||
flat = true;
|
||||
|
||||
for (Map.Entry<String, String> e : this.textures.entrySet()) {
|
||||
|
||||
// If there is a value in the key set, replace with the value it points to
|
||||
// Also avoid adding a loop
|
||||
if (this.textures.containsKey(e.getValue())) {
|
||||
|
||||
if (!e.getKey().equals(e.getValue())) tmp.put(e.getKey(), this.textures.get(e.getValue()));
|
||||
else tmp.put(e.getKey(), "");
|
||||
flat = false;
|
||||
} else {
|
||||
tmp.put(e.getKey(), e.getValue());
|
||||
}
|
||||
}
|
||||
this.textures.putAll(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, String> getTextures() {
|
||||
return Object2ObjectMaps.unmodifiable(textures);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.unbaked;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.baked.PileOfQuads.BLANK;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.BakeData;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.baked.BakedModel;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
|
||||
|
||||
public class MissingModel extends JSONModel {
|
||||
|
||||
public static final MissingModel MISSING_MODEL = new MissingModel();
|
||||
|
||||
public MissingModel() {
|
||||
super(null, false, null, Object2ObjectMaps.emptyMap(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BakedModel bake(BakeData data) {
|
||||
return BLANK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.unbaked;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.BakeData;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.JSONVariant;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.Weighted;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.baked.BakedModel;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.baked.MonopartModel;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
|
||||
public final class MonopartDough implements UnbakedModel {
|
||||
|
||||
private final ObjectList<Weighted<JSONVariant>> variants;
|
||||
|
||||
public MonopartDough(ObjectList<Weighted<JSONVariant>> variants) {
|
||||
this.variants = variants;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BakedModel bake(BakeData data) {
|
||||
// Load and bake the models
|
||||
var models = new ObjectArrayList<Weighted<BakedModel>>(variants.size());
|
||||
for (var v : variants) {
|
||||
final var model = ModelRegistry.getJSONModel(v.thing().model());
|
||||
final var bread = model.bake(v.thing());
|
||||
models.add(new Weighted<>(bread, v.weight()));
|
||||
}
|
||||
|
||||
if (models.size() == 1) return models.get(0).thing();
|
||||
return new MonopartModel(models);
|
||||
}
|
||||
|
||||
public ObjectList<Weighted<JSONVariant>> variants() {
|
||||
return variants;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.model.unbaked;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.BakeData;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.baked.BakedModel;
|
||||
|
||||
public interface UnbakedModel {
|
||||
|
||||
default BakedModel bake() {
|
||||
return bake(BakeData.IDENTITY);
|
||||
}
|
||||
|
||||
BakedModel bake(BakeData data);
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.COLOR_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.LIGHT_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.NORMAL_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.POSITION_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.TEXTURE_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.vertexOffset;
|
||||
import static net.minecraft.util.MathHelper.clamp_int;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
@@ -16,8 +22,8 @@ import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.BlockPos;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Quad;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuad;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.stacks.Vector3dStack;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.util.ObjectPooler;
|
||||
@@ -25,20 +31,19 @@ import com.gtnewhorizon.gtnhlib.util.ObjectPooler;
|
||||
import cpw.mods.fml.relauncher.ReflectionHelper;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
/*
|
||||
* To be used in conjunction with the TessellatorManager Used to capture the quads generated by the Tessellator across
|
||||
* multiple draw calls and make the quad list available for usage. NOTE: This will _not_ (currently) capture, integrate,
|
||||
* or stop any GL calls made around the tessellator draw calls.
|
||||
*/
|
||||
/// To be used in conjunction with the {@link TessellatorManager}. Used to capture the quads generated by the
|
||||
/// {@link Tessellator} across multiple draw calls and make the quad list available for usage.
|
||||
///
|
||||
/// NOTE: This will _not_ (currently) capture, integrate, or stop any GL calls made around the tessellator draw calls.
|
||||
@SuppressWarnings("unused")
|
||||
public class CapturingTessellator extends Tessellator implements ITessellatorInstance {
|
||||
|
||||
// Access Transformers don't work on Forge Fields :rage:
|
||||
private static final MethodHandle sRawBufferSize;
|
||||
private static final MethodHandle gRawBufferSize;
|
||||
private final Quad.Flags FLAGS = new Quad.Flags(true, true, true, true);
|
||||
private final ObjectPooler<QuadView> quadBuf = new ObjectPooler<>(Quad::new);
|
||||
private final List<QuadView> collectedQuads = new ObjectArrayList<>();
|
||||
private final Flags FLAGS = new Flags(true, true, true, true);
|
||||
private final ObjectPooler<ModelQuad> quadBuf = new ObjectPooler<>(ModelQuad::new);
|
||||
private final List<ModelQuadViewMutable> collectedQuads = new ObjectArrayList<>();
|
||||
private int shaderBlockId = -1;
|
||||
|
||||
// Any offset we need to the Tesselator's offset!
|
||||
@@ -82,6 +87,18 @@ public class CapturingTessellator extends Tessellator implements ITessellatorIns
|
||||
}
|
||||
}
|
||||
|
||||
private int dataX(int offset, int vi) {
|
||||
return rawBuffer[offset + vertexOffset(vi)];
|
||||
}
|
||||
|
||||
private int dataY(int offset, int vi) {
|
||||
return rawBuffer[offset + vertexOffset(vi) + 1];
|
||||
}
|
||||
|
||||
private int dataZ(int offset, int vi) {
|
||||
return rawBuffer[offset + vertexOffset(vi) + 2];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int draw() {
|
||||
// Adapted from Neodymium
|
||||
@@ -93,22 +110,13 @@ public class CapturingTessellator extends Tessellator implements ITessellatorIns
|
||||
final int verticesPerPrimitive = this.drawMode == GL11.GL_QUADS ? 4 : 3;
|
||||
|
||||
for (int quadI = 0; quadI < this.vertexCount / verticesPerPrimitive; quadI++) {
|
||||
final QuadView quad = quadBuf.getInstance();
|
||||
quad.setState(
|
||||
this.rawBuffer,
|
||||
quadI * (verticesPerPrimitive * 8),
|
||||
FLAGS,
|
||||
this.drawMode,
|
||||
-offset.x,
|
||||
-offset.y,
|
||||
-offset.z);
|
||||
quad.setShaderBlockId(shaderBlockId);
|
||||
final int srcOffset = quadI * (verticesPerPrimitive * 8);
|
||||
if (empty(srcOffset)) continue;
|
||||
|
||||
if (quad.isDeleted()) {
|
||||
quadBuf.releaseInstance(quad);
|
||||
} else {
|
||||
this.collectedQuads.add(quad);
|
||||
}
|
||||
final ModelQuad quad = quadBuf.getInstance();
|
||||
quad.setState(this.rawBuffer, srcOffset, FLAGS, this.drawMode, -offset.x, -offset.y, -offset.z);
|
||||
quad.setShaderBlockId(shaderBlockId);
|
||||
this.collectedQuads.add(quad);
|
||||
}
|
||||
|
||||
final int i = this.rawBufferIndex * 4;
|
||||
@@ -116,25 +124,34 @@ public class CapturingTessellator extends Tessellator implements ITessellatorIns
|
||||
return i;
|
||||
}
|
||||
|
||||
private boolean empty(int srcOffset) {
|
||||
return dataX(srcOffset, 0) == dataX(srcOffset, 1) && dataX(srcOffset, 1) == dataX(srcOffset, 2)
|
||||
&& dataX(srcOffset, 2) == dataX(srcOffset, 3)
|
||||
&& dataY(srcOffset, 0) == dataY(srcOffset, 1)
|
||||
&& dataY(srcOffset, 1) == dataY(srcOffset, 2)
|
||||
&& dataY(srcOffset, 2) == dataY(srcOffset, 3);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void discard() {
|
||||
isDrawing = false;
|
||||
reset();
|
||||
}
|
||||
|
||||
public List<QuadView> getQuads() {
|
||||
public List<ModelQuadViewMutable> getQuads() {
|
||||
return collectedQuads;
|
||||
}
|
||||
|
||||
public void clearQuads() {
|
||||
// noinspection ForLoopReplaceableByForEach
|
||||
for (int i = 0; i < this.collectedQuads.size(); i++) {
|
||||
this.quadBuf.releaseInstance(this.collectedQuads.get(i));
|
||||
for (int i = 0; i < collectedQuads.size(); i++) {
|
||||
final var quad = collectedQuads.get(i);
|
||||
if (quad instanceof ModelQuad mq) quadBuf.releaseInstance(mq);
|
||||
}
|
||||
this.collectedQuads.clear();
|
||||
collectedQuads.clear();
|
||||
}
|
||||
|
||||
public static ByteBuffer quadsToBuffer(List<QuadView> quads, VertexFormat format) {
|
||||
public static ByteBuffer quadsToBuffer(List<ModelQuadViewMutable> quads, VertexFormat format) {
|
||||
if (!format.canWriteQuads()) {
|
||||
throw new IllegalStateException("Vertex format has no quad writer: " + format);
|
||||
}
|
||||
@@ -169,16 +186,16 @@ public class CapturingTessellator extends Tessellator implements ITessellatorIns
|
||||
public CapturingTessellator pos(double x, double y, double z) {
|
||||
ensureBuffer();
|
||||
|
||||
this.rawBuffer[this.rawBufferIndex + 0] = Float.floatToRawIntBits((float) (x + this.xOffset));
|
||||
this.rawBuffer[this.rawBufferIndex + 1] = Float.floatToRawIntBits((float) (y + this.yOffset));
|
||||
this.rawBuffer[this.rawBufferIndex + 2] = Float.floatToRawIntBits((float) (z + this.zOffset));
|
||||
this.rawBuffer[this.rawBufferIndex + POSITION_INDEX + 0] = Float.floatToRawIntBits((float) (x + this.xOffset));
|
||||
this.rawBuffer[this.rawBufferIndex + POSITION_INDEX + 1] = Float.floatToRawIntBits((float) (y + this.yOffset));
|
||||
this.rawBuffer[this.rawBufferIndex + POSITION_INDEX + 2] = Float.floatToRawIntBits((float) (z + this.zOffset));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public CapturingTessellator tex(double u, double v) {
|
||||
this.rawBuffer[this.rawBufferIndex + 3] = Float.floatToRawIntBits((float) u);
|
||||
this.rawBuffer[this.rawBufferIndex + 4] = Float.floatToRawIntBits((float) v);
|
||||
this.rawBuffer[this.rawBufferIndex + TEXTURE_INDEX] = Float.floatToRawIntBits((float) u);
|
||||
this.rawBuffer[this.rawBufferIndex + TEXTURE_INDEX + 1] = Float.floatToRawIntBits((float) v);
|
||||
this.hasTexture = true;
|
||||
|
||||
return this;
|
||||
@@ -201,7 +218,7 @@ public class CapturingTessellator extends Tessellator implements ITessellatorIns
|
||||
} else {
|
||||
color = red << 24 | green << 16 | blue << 8 | alpha;
|
||||
}
|
||||
this.rawBuffer[this.rawBufferIndex + 5] = color;
|
||||
this.rawBuffer[this.rawBufferIndex + COLOR_INDEX] = color;
|
||||
this.hasColor = true;
|
||||
|
||||
return this;
|
||||
@@ -213,7 +230,7 @@ public class CapturingTessellator extends Tessellator implements ITessellatorIns
|
||||
final byte b1 = (byte) ((int) (y * 127.0F));
|
||||
final byte b2 = (byte) ((int) (z * 127.0F));
|
||||
|
||||
this.rawBuffer[this.rawBufferIndex + 6] = b0 & 255 | (b1 & 255) << 8 | (b2 & 255) << 16;
|
||||
this.rawBuffer[this.rawBufferIndex + NORMAL_INDEX] = b0 & 255 | (b1 & 255) << 8 | (b2 & 255) << 16;
|
||||
this.hasNormals = true;
|
||||
return this;
|
||||
}
|
||||
@@ -223,7 +240,7 @@ public class CapturingTessellator extends Tessellator implements ITessellatorIns
|
||||
}
|
||||
|
||||
public CapturingTessellator brightness(int brightness) {
|
||||
this.rawBuffer[this.rawBufferIndex + 7] = brightness;
|
||||
this.rawBuffer[this.rawBufferIndex + LIGHT_INDEX] = brightness;
|
||||
this.hasBrightness = true;
|
||||
|
||||
return this;
|
||||
@@ -262,4 +279,19 @@ public class CapturingTessellator extends Tessellator implements ITessellatorIns
|
||||
shaderBlockId = blockId;
|
||||
}
|
||||
|
||||
public static class Flags {
|
||||
|
||||
boolean hasTexture;
|
||||
public boolean hasBrightness;
|
||||
public boolean hasColor;
|
||||
public boolean hasNormals;
|
||||
|
||||
public Flags(boolean hasTexture, boolean hasBrightness, boolean hasColor, boolean hasNormals) {
|
||||
this.hasTexture = hasTexture;
|
||||
this.hasBrightness = hasBrightness;
|
||||
this.hasColor = hasColor;
|
||||
this.hasNormals = hasNormals;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import net.minecraft.client.renderer.Tessellator;
|
||||
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
|
||||
@@ -48,18 +48,16 @@ public class TessellatorManager {
|
||||
if (currentlyCapturing.get())
|
||||
throw new IllegalStateException("Tried to start capturing when already capturing!");
|
||||
final CapturingTessellator tess = capturingTessellator.get();
|
||||
if (tess.getQuads().size() > 0)
|
||||
if (!tess.getQuads().isEmpty())
|
||||
throw new IllegalStateException("Tried to start capturing with existing collected Quads!");
|
||||
tess.storeTranslation();
|
||||
|
||||
currentlyCapturing.set(true);
|
||||
}
|
||||
|
||||
/*
|
||||
* Stop the CapturingTessellator and return the pooled quads. The quads are valid until clearQuads() is called on
|
||||
* the CapturingTesselator, which must be done before starting capturing again.
|
||||
*/
|
||||
public static List<QuadView> stopCapturingToPooledQuads() {
|
||||
/// Stop the CapturingTessellator and return the pooled quads. The quads are valid until clearQuads() is called on
|
||||
/// the CapturingTesselator, which must be done before starting capturing again.
|
||||
public static List<ModelQuadViewMutable> stopCapturingToPooledQuads() {
|
||||
if (!currentlyCapturing.get()) throw new IllegalStateException("Tried to stop capturing when not capturing!");
|
||||
currentlyCapturing.set(false);
|
||||
final CapturingTessellator tess = capturingTessellator.get();
|
||||
@@ -67,27 +65,23 @@ public class TessellatorManager {
|
||||
// Be sure we got all the quads
|
||||
if (tess.isDrawing) tess.draw();
|
||||
|
||||
final List<QuadView> quads = tess.getQuads();
|
||||
final var quads = tess.getQuads();
|
||||
tess.discard();
|
||||
tess.restoreTranslation();
|
||||
|
||||
return quads;
|
||||
}
|
||||
|
||||
/*
|
||||
* Stops the CapturingTessellator, stores the quads in a buffer (based on the VertexFormat provided), and clears the
|
||||
* quads.
|
||||
*/
|
||||
/// Stops the CapturingTessellator, stores the quads in a buffer (based on the VertexFormat provided), and clears
|
||||
/// the quads.
|
||||
public static ByteBuffer stopCapturingToBuffer(VertexFormat format) {
|
||||
final ByteBuffer buf = CapturingTessellator.quadsToBuffer(stopCapturingToPooledQuads(), format);
|
||||
capturingTessellator.get().clearQuads();
|
||||
return buf;
|
||||
}
|
||||
|
||||
/*
|
||||
* Stops the CapturingTessellator, stores the quads in a buffer (based on the VertexFormat provided), uploads the
|
||||
* buffer to a new VertexBuffer, and clears the quads.
|
||||
*/
|
||||
/// Stops the CapturingTessellator, stores the quads in a buffer (based on the VertexFormat provided), uploads the
|
||||
/// buffer to a new VertexBuffer, and clears the quads.
|
||||
public static VertexBuffer stopCapturingToVBO(VertexFormat format) {
|
||||
return new VertexBuffer(format, GL11.GL_QUADS).upload(stopCapturingToBuffer(format));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util;
|
||||
|
||||
/// Provides some utilities for packing and unpacking color components from packed integer colors in ABGR format, which
|
||||
/// is used by OpenGL for color vectors.
|
||||
///
|
||||
/// |||||
|
||||
/// | --------- | --------- | --------- | ---------- |
|
||||
/// | 32 | 24 | 16 | 8 |
|
||||
/// | 0110 1100 | 0110 1100 | 0110 1100 | 0110 1100 |
|
||||
/// | Alpha | Blue | Green | Red |
|
||||
public class ColorABGR implements ColorU8 {
|
||||
|
||||
private static final int RED_COMPONENT_OFFSET = 0;
|
||||
private static final int GREEN_COMPONENT_OFFSET = 8;
|
||||
private static final int BLUE_COMPONENT_OFFSET = 16;
|
||||
private static final int ALPHA_COMPONENT_OFFSET = 24;
|
||||
|
||||
/**
|
||||
* Packs the specified color components into ABGR format. The alpha component is fully opaque.
|
||||
*
|
||||
* @param r The red component of the color
|
||||
* @param g The green component of the color
|
||||
* @param b The blue component of the color
|
||||
*/
|
||||
public static int pack(float r, float g, float b) {
|
||||
return pack(r, g, b, COMPONENT_MASK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs the specified color components into ABGR format.
|
||||
*
|
||||
* @param r The red component of the color
|
||||
* @param g The green component of the color
|
||||
* @param b The blue component of the color
|
||||
* @param a The alpha component of the color
|
||||
*/
|
||||
public static int pack(int r, int g, int b, int a) {
|
||||
return ((a & COMPONENT_MASK) << ALPHA_COMPONENT_OFFSET) | ((b & COMPONENT_MASK) << BLUE_COMPONENT_OFFSET)
|
||||
| ((g & COMPONENT_MASK) << GREEN_COMPONENT_OFFSET)
|
||||
| ((r & COMPONENT_MASK) << RED_COMPONENT_OFFSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs the specified color components into ABGR format.
|
||||
*
|
||||
* @param rgb The red/green/blue component of the color
|
||||
* @param alpha The alpha component of the color
|
||||
*/
|
||||
public static int withAlpha(int rgb, float alpha) {
|
||||
return withAlpha(rgb, ColorU8.normalizedFloatToByte(alpha));
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs the specified color components into ABGR format.
|
||||
*
|
||||
* @param rgb The red/green/blue component of the color
|
||||
* @param alpha The alpha component of the color
|
||||
*/
|
||||
public static int withAlpha(int rgb, int alpha) {
|
||||
return (alpha << ALPHA_COMPONENT_OFFSET) | (rgb & ~(COMPONENT_MASK << ALPHA_COMPONENT_OFFSET));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the color components from normalized floating point into a packed integer in ABGR format.
|
||||
*
|
||||
* @see ColorABGR#pack(int, int, int, int)
|
||||
*/
|
||||
public static int pack(float r, float g, float b, float a) {
|
||||
return pack(
|
||||
ColorU8.normalizedFloatToByte(r),
|
||||
ColorU8.normalizedFloatToByte(g),
|
||||
ColorU8.normalizedFloatToByte(b),
|
||||
ColorU8.normalizedFloatToByte(a));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param color The packed 32-bit ABGR color to unpack
|
||||
* @return The red color component in the range of 0..255
|
||||
*/
|
||||
public static int unpackRed(int color) {
|
||||
return (color >> RED_COMPONENT_OFFSET) & COMPONENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param color The packed 32-bit ABGR color to unpack
|
||||
* @return The green color component in the range of 0..255
|
||||
*/
|
||||
public static int unpackGreen(int color) {
|
||||
return (color >> GREEN_COMPONENT_OFFSET) & COMPONENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param color The packed 32-bit ABGR color to unpack
|
||||
* @return The blue color component in the range of 0..255
|
||||
*/
|
||||
public static int unpackBlue(int color) {
|
||||
return (color >> BLUE_COMPONENT_OFFSET) & COMPONENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param color The packed 32-bit ABGR color to unpack
|
||||
* @return The red color component in the range of 0..255
|
||||
*/
|
||||
public static int unpackAlpha(int color) {
|
||||
return (color >> ALPHA_COMPONENT_OFFSET) & COMPONENT_MASK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util;
|
||||
|
||||
/// Provides some utilities for packing and unpacking color components from packed integer colors in ARGB format. This
|
||||
/// packed format is used by most of Minecraft, but special care must be taken to pack it into ABGR format before
|
||||
/// passing
|
||||
/// it to OpenGL attributes.
|
||||
///
|
||||
/// |||||
|
||||
/// | --------- | --------- | --------- | ---------- |
|
||||
/// | 32 | 24 | 16 | 8 |
|
||||
/// | 0110 1100 | 0110 1100 | 0110 1100 | 0110 1100 |
|
||||
/// | Alpha | Red | Green | Blue |
|
||||
public class ColorARGB implements ColorU8 {
|
||||
|
||||
private static final int ALPHA_COMPONENT_OFFSET = 24;
|
||||
private static final int RED_COMPONENT_OFFSET = 16;
|
||||
private static final int GREEN_COMPONENT_OFFSET = 8;
|
||||
private static final int BLUE_COMPONENT_OFFSET = 0;
|
||||
|
||||
/**
|
||||
* Packs the specified color components into big-endian format for consumption by OpenGL.
|
||||
*
|
||||
* @param r The red component of the color
|
||||
* @param g The green component of the color
|
||||
* @param b The blue component of the color
|
||||
* @param a The alpha component of the color
|
||||
*/
|
||||
public static int pack(int r, int g, int b, int a) {
|
||||
return (a & COMPONENT_MASK) << ALPHA_COMPONENT_OFFSET | (r & COMPONENT_MASK) << RED_COMPONENT_OFFSET
|
||||
| (g & COMPONENT_MASK) << GREEN_COMPONENT_OFFSET
|
||||
| (b & COMPONENT_MASK) << BLUE_COMPONENT_OFFSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs the specified color components into big-endian format for consumption by OpenGL. The alpha channel is fully
|
||||
* opaque.
|
||||
*
|
||||
* @param r The red component of the color
|
||||
* @param g The green component of the color
|
||||
* @param b The blue component of the color
|
||||
*/
|
||||
public static int pack(int r, int g, int b) {
|
||||
return pack(r, g, b, (1 << ColorU8.COMPONENT_BITS) - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param color The packed 32-bit ARGB color to unpack
|
||||
* @return The red color component in the range of 0..255
|
||||
*/
|
||||
public static int unpackAlpha(int color) {
|
||||
return color >> ALPHA_COMPONENT_OFFSET & COMPONENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param color The packed 32-bit ARGB color to unpack
|
||||
* @return The red color component in the range of 0..255
|
||||
*/
|
||||
public static int unpackRed(int color) {
|
||||
return color >> RED_COMPONENT_OFFSET & COMPONENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param color The packed 32-bit ARGB color to unpack
|
||||
* @return The green color component in the range of 0..255
|
||||
*/
|
||||
public static int unpackGreen(int color) {
|
||||
return color >> GREEN_COMPONENT_OFFSET & COMPONENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param color The packed 32-bit ARGB color to unpack
|
||||
* @return The blue color component in the range of 0..255
|
||||
*/
|
||||
public static int unpackBlue(int color) {
|
||||
return color >> BLUE_COMPONENT_OFFSET & COMPONENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-packs the ARGB color into a aBGR color with the specified alpha component.
|
||||
*/
|
||||
public static int toABGR(int color, int alpha) {
|
||||
return Integer.reverseBytes(color << 8 | alpha);
|
||||
}
|
||||
|
||||
public static int toABGR(int color) {
|
||||
return Integer.reverseBytes(Integer.rotateLeft(color, 8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs the specified color components into ARGB format.
|
||||
*
|
||||
* @param rgb The red/green/blue component of the color
|
||||
* @param alpha The alpha component of the color
|
||||
*/
|
||||
public static int withAlpha(int rgb, int alpha) {
|
||||
return (alpha << ALPHA_COMPONENT_OFFSET) | (rgb & ~(COMPONENT_MASK << ALPHA_COMPONENT_OFFSET));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util;
|
||||
|
||||
public class ColorMixer {
|
||||
|
||||
private static final int CHANNEL_MASK = 0x00FF00FF;
|
||||
|
||||
/**
|
||||
* Mixes a 32-bit color (with packed 8-bit components) into another using the given ratio. This is equivalent to
|
||||
* (color1 * ratio) + (color2 * (1.0 - ratio)) but uses bitwise trickery for maximum performance.
|
||||
* <p>
|
||||
* The order of the channels within the packed color does not matter, and the output color will always have the same
|
||||
* ordering as the input colors.
|
||||
*
|
||||
* @param aColor The color to mix towards
|
||||
* @param bColor The color to mix away from
|
||||
* @param ratio The percentage (in 0.0..1.0 range) to mix the first color into the second color
|
||||
* @return The mixed color in packed 32-bit format
|
||||
*/
|
||||
public static int mix(int aColor, int bColor, float ratio) {
|
||||
int aRatio = (int) (256 * ratio); // int(ratio)
|
||||
int bRatio = 256 - aRatio; // int(1.0 - ratio)
|
||||
|
||||
// Mask off and shift two components from each color into a packed vector of 16-bit components, where the
|
||||
// high 8 bits are all zeroes.
|
||||
int a1 = (aColor >> 0) & CHANNEL_MASK;
|
||||
int b1 = (bColor >> 0) & CHANNEL_MASK;
|
||||
int a2 = (aColor >> 8) & CHANNEL_MASK;
|
||||
int b2 = (bColor >> 8) & CHANNEL_MASK;
|
||||
|
||||
// Multiply the packed 16-bit components against each mix factor, and add the components of each color
|
||||
// to produce the mixed result. This will never overflow since both 16-bit integers are in 0..255 range.
|
||||
|
||||
// Then, shift the high 8 bits of each packed 16-bit component into the low 8 bits, and mask off the high bits
|
||||
// of each 16-bit component to produce a vector of packed 8-bit components, where every other component is
|
||||
// empty.
|
||||
int c1 = (((a1 * aRatio) + (b1 * bRatio)) >> 8) & CHANNEL_MASK;
|
||||
int c2 = (((a2 * aRatio) + (b2 * bRatio)) >> 8) & CHANNEL_MASK;
|
||||
|
||||
// Join the color components into the original order
|
||||
return ((c1 << 0) | (c2 << 8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies the 32-bit colors (with packed 8-bit components) together.
|
||||
* <p>
|
||||
* The order of the channels within the packed color does not matter, and the output color will always have the same
|
||||
* ordering as the input colors.
|
||||
*
|
||||
* @param a The first color to multiply
|
||||
* @param b The second color to multiply
|
||||
* @return The multiplied color in packed 32-bit format
|
||||
*/
|
||||
public static int mul(int a, int b) {
|
||||
// Take each 8-bit component pair, multiply them together to create intermediate 16-bit integers,
|
||||
// and then shift the high half of each 16-bit integer into 8-bit integers.
|
||||
int c0 = (((a >> 0) & 0xFF) * ((b >> 0) & 0xFF)) >> 8;
|
||||
int c1 = (((a >> 8) & 0xFF) * ((b >> 8) & 0xFF)) >> 8;
|
||||
int c2 = (((a >> 16) & 0xFF) * ((b >> 16) & 0xFF)) >> 8;
|
||||
int c3 = (((a >> 24) & 0xFF) * ((b >> 24) & 0xFF)) >> 8;
|
||||
|
||||
// Pack the components
|
||||
return (c0 << 0) | (c1 << 8) | (c2 << 16) | (c3 << 24);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies the RGB channels of a 32-bit color with packed 8-bit components by an 8-bit component.
|
||||
* <p>
|
||||
* Aside from alpha being in the most significant 8 bits, the order of the channels within the packed color does not
|
||||
* matter, and the output color will always have the same ordering as the input colors.
|
||||
*
|
||||
* @param a The first color to multiply
|
||||
* @param b The component to multiply the RGB components by
|
||||
* @return The multiplied color in packed 32-bit format
|
||||
*/
|
||||
public static int mulSingleWithoutAlpha(int a, int b) {
|
||||
b &= 0xFF; // Mask the component for safety
|
||||
// Take each 8-bit component pair, multiply them together to create intermediate 16-bit integers,
|
||||
// and then shift the high half of each 16-bit integer into 8-bit integers.
|
||||
int c0 = (((a >> 0) & 0xFF) * b) >> 8;
|
||||
int c1 = (((a >> 8) & 0xFF) * b) >> 8;
|
||||
int c2 = (((a >> 16) & 0xFF) * b) >> 8;
|
||||
int c3 = (a >> 24) & 0xFF;
|
||||
|
||||
// Pack the components
|
||||
return (c0 << 0) | (c1 << 8) | (c2 << 16) | (c3 << 24);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util;
|
||||
|
||||
public interface ColorU8 {
|
||||
|
||||
/**
|
||||
* The number of bits used for each color component.
|
||||
*/
|
||||
int COMPONENT_BITS = 8;
|
||||
|
||||
/**
|
||||
* The bitwise mask for each color component.
|
||||
*/
|
||||
int COMPONENT_MASK = (1 << COMPONENT_BITS) - 1;
|
||||
|
||||
/**
|
||||
* The maximum value of a color component. Used for converting normalizing floats to integers.
|
||||
*/
|
||||
float COMPONENT_RANGE = (float) COMPONENT_MASK;
|
||||
|
||||
/**
|
||||
* The multiplicative inverse of (value / COMPONENT_RANGE). Used for converting integers to normalized floats.
|
||||
*/
|
||||
float COMPONENT_RANGE_INVERSE = 1.0f / COMPONENT_RANGE;
|
||||
|
||||
/**
|
||||
* Converts a normalized float to an integer component.
|
||||
*
|
||||
* @param value The floating point value in the range of 0.0..1.0
|
||||
* @return The integer component of the floating point value in 0..255 range
|
||||
*/
|
||||
static int normalizedFloatToByte(float value) {
|
||||
return (int) (value * COMPONENT_RANGE) & COMPONENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an integer component to a normalized floating point value.
|
||||
*
|
||||
* @param value The integer component in 0..255 range
|
||||
* @return The floating point value of the integer component in the range of 0.0..1.0
|
||||
*/
|
||||
static float byteToNormalizedFloat(int value) {
|
||||
return (float) value * COMPONENT_RANGE_INVERSE;
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -1,15 +1,17 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.util;
|
||||
|
||||
import net.minecraft.util.MathHelper;
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util;
|
||||
|
||||
import org.joml.Math;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
/**
|
||||
* Provides some utilities for working with packed normal vectors. Each normal component provides 8 bits of precision in
|
||||
* the range of [-1.0,1.0].
|
||||
*
|
||||
* | 32 | 24 | 16 | 8 | | 0000 0000 | 0110 1100 | 0110 1100 | 0110 1100 | | Padding | X | Y | Z |
|
||||
*/
|
||||
/// Provides some utilities for working with packed normal vectors. Each normal component provides 8 bits of precision
|
||||
/// in
|
||||
/// the range of \[-1.0,1.0].
|
||||
///
|
||||
/// |||||
|
||||
/// | --------- | --------- | --------- | ---------- |
|
||||
/// | 32 | 24 | 16 | 8 |
|
||||
/// | 0000 0000 | 0110 1100 | 0110 1100 | 0110 1100 |
|
||||
/// | Padding | X | Y | Z |
|
||||
public class NormI8 {
|
||||
|
||||
private static final int X_COMPONENT_OFFSET = 0;
|
||||
@@ -53,7 +55,7 @@ public class NormI8 {
|
||||
*/
|
||||
private static int encode(float comp) {
|
||||
// TODO: is the clamp necessary here? our inputs should always be normalized vector components
|
||||
return ((int) (MathHelper.clamp_float(comp, -1.0F, 1.0F) * COMPONENT_RANGE) & 255);
|
||||
return ((int) (Math.clamp(comp, -1.0F, 1.0F) * COMPONENT_RANGE) & 255);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,261 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.COLOR_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.DEFAULT_COLOR;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.DEFAULT_LIGHTMAP;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.LIGHT_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.NORMAL_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.POSITION_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.TEXTURE_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.VERTEX_SIZE;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.vertexOffset;
|
||||
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator.Flags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil;
|
||||
|
||||
/// A simple implementation of the [ModelQuadViewMutable] interface which can provide an on-heap scratch area
|
||||
/// for storing quad vertex data.
|
||||
public class ModelQuad implements ModelQuadViewMutable {
|
||||
|
||||
private final int[] data = new int[VERTEX_SIZE * 4];
|
||||
private int flags;
|
||||
|
||||
private int normal;
|
||||
|
||||
private Object sprite;
|
||||
private int colorIdx;
|
||||
private ModelQuadFacing direction;
|
||||
|
||||
private boolean hasAmbientOcclusion = true;
|
||||
private int shaderBlockId;
|
||||
|
||||
@Override
|
||||
public void setX(int idx, float x) {
|
||||
this.data[vertexOffset(idx) + POSITION_INDEX] = Float.floatToRawIntBits(x);
|
||||
this.normal = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setY(int idx, float y) {
|
||||
this.data[vertexOffset(idx) + POSITION_INDEX + 1] = Float.floatToRawIntBits(y);
|
||||
this.normal = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZ(int idx, float z) {
|
||||
this.data[vertexOffset(idx) + POSITION_INDEX + 2] = Float.floatToRawIntBits(z);
|
||||
this.normal = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColor(int idx, int color) {
|
||||
this.data[vertexOffset(idx) + COLOR_INDEX] = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTexU(int idx, float u) {
|
||||
this.data[vertexOffset(idx) + TEXTURE_INDEX] = Float.floatToRawIntBits(u);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTexV(int idx, float v) {
|
||||
this.data[vertexOffset(idx) + TEXTURE_INDEX + 1] = Float.floatToRawIntBits(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLight(int idx, int light) {
|
||||
this.data[vertexOffset(idx) + LIGHT_INDEX] = light;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFlags(int flags) {
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSprite(Object sprite) {
|
||||
this.sprite = sprite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorIndex(int index) {
|
||||
this.colorIdx = index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLightFace(ModelQuadFacing face) {
|
||||
if (!face.isDirection()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
this.direction = face;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHasAmbientOcclusion(boolean hasAmbientOcclusion) {
|
||||
this.hasAmbientOcclusion = hasAmbientOcclusion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setForgeNormal(int idx, int normal) {
|
||||
this.data[vertexOffset(idx) + NORMAL_INDEX] = normal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShaderBlockId(int shaderBlockId) {
|
||||
this.shaderBlockId = shaderBlockId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColorIndex() {
|
||||
return this.colorIdx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getX(int idx) {
|
||||
return Float.intBitsToFloat(this.data[vertexOffset(idx) + POSITION_INDEX]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getY(int idx) {
|
||||
return Float.intBitsToFloat(this.data[vertexOffset(idx) + POSITION_INDEX + 1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getZ(int idx) {
|
||||
return Float.intBitsToFloat(this.data[vertexOffset(idx) + POSITION_INDEX + 2]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor(int idx) {
|
||||
return this.data[vertexOffset(idx) + COLOR_INDEX];
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTexU(int idx) {
|
||||
return Float.intBitsToFloat(this.data[vertexOffset(idx) + TEXTURE_INDEX]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTexV(int idx) {
|
||||
return Float.intBitsToFloat(this.data[vertexOffset(idx) + TEXTURE_INDEX + 1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLight(int idx) {
|
||||
return this.data[vertexOffset(idx) + LIGHT_INDEX];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getForgeNormal(int idx) {
|
||||
return this.data[vertexOffset(idx) + NORMAL_INDEX];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getComputedFaceNormal() {
|
||||
int n = this.normal;
|
||||
if (n == 0) {
|
||||
this.normal = n = ModelQuadUtil.calculateNormal(this);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelQuadFacing getNormalFace() {
|
||||
return ModelQuadUtil.findNormalFace(getComputedFaceNormal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFlags() {
|
||||
return this.flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object celeritas$getSprite() {
|
||||
return this.sprite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelQuadFacing getLightFace() {
|
||||
return this.direction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAmbientOcclusion() {
|
||||
return this.hasAmbientOcclusion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getShaderBlockId() {
|
||||
return this.shaderBlockId;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void setState(int[] rawBuffer, int srcOffset, Flags flags, int drawMode, int offsetX, int offsetY,
|
||||
int offsetZ) {
|
||||
System.arraycopy(rawBuffer, srcOffset, data, 0, data.length);
|
||||
|
||||
if (!flags.hasColor) clearColors();
|
||||
if (!flags.hasNormals) this.clearNormals();
|
||||
if (!flags.hasBrightness) this.clearLightmap();
|
||||
// TODO confirm this is correct
|
||||
setHasAmbientOcclusion(flags.hasBrightness);
|
||||
|
||||
offsetPos(0, offsetX);
|
||||
offsetPos(1, offsetY);
|
||||
offsetPos(2, offsetZ);
|
||||
|
||||
if (drawMode == GL_TRIANGLES) quadrangulate();
|
||||
|
||||
setLightFace(ModelQuadUtil.findLightFace(getComputedFaceNormal()));
|
||||
}
|
||||
|
||||
/// Copies the third vertex to the fourth, turning this into a degenerate quad. Useful for faking triangle support.
|
||||
private void quadrangulate() {
|
||||
System.arraycopy(data, vertexOffset(2), data, vertexOffset(3), VERTEX_SIZE);
|
||||
}
|
||||
|
||||
/// Shifts every vertex by the given amount.
|
||||
/// @param idx The index of the position coord to offset. 0 = x, 1 = y, 2 = z.
|
||||
/// @param offset The amount to shift the coord by.
|
||||
private void offsetPos(int idx, float offset) {
|
||||
final int i = POSITION_INDEX + idx;
|
||||
setData(0, i, getData(0, i) + offset);
|
||||
setData(1, i, getData(1, i) + offset);
|
||||
setData(2, i, getData(2, i) + offset);
|
||||
setData(3, i, getData(3, i) + offset);
|
||||
}
|
||||
|
||||
private float getData(int vi, int i) {
|
||||
return Float.intBitsToFloat(data[vertexOffset(vi) + i]);
|
||||
}
|
||||
|
||||
private void setData(int vi, int i, float val) {
|
||||
data[vertexOffset(vi) + i] = Float.floatToIntBits(val);
|
||||
}
|
||||
|
||||
private void clearColors() {
|
||||
setColor(0, DEFAULT_COLOR);
|
||||
setColor(1, DEFAULT_COLOR);
|
||||
setColor(2, DEFAULT_COLOR);
|
||||
setColor(3, DEFAULT_COLOR);
|
||||
}
|
||||
|
||||
private void clearNormals() {
|
||||
normal = 0;
|
||||
data[vertexOffset(0) + NORMAL_INDEX] = 0;
|
||||
data[vertexOffset(1) + NORMAL_INDEX] = 0;
|
||||
data[vertexOffset(2) + NORMAL_INDEX] = 0;
|
||||
data[vertexOffset(3) + NORMAL_INDEX] = 0;
|
||||
}
|
||||
|
||||
private void clearLightmap() {
|
||||
setLight(0, DEFAULT_LIGHTMAP);
|
||||
setLight(0, DEFAULT_LIGHTMAP);
|
||||
setLight(0, DEFAULT_LIGHTMAP);
|
||||
setLight(0, DEFAULT_LIGHTMAP);
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFlags;
|
||||
|
||||
/**
|
||||
* Provides a read-only view of a model quad. For mutable access to a model quad, see {@link ModelQuadViewMutable}.
|
||||
*/
|
||||
public interface ModelQuadView {
|
||||
|
||||
/**
|
||||
* @return The x-position of the vertex at index {@param idx}
|
||||
*/
|
||||
float getX(int idx);
|
||||
|
||||
/**
|
||||
* @return The y-position of the vertex at index {@param idx}
|
||||
*/
|
||||
float getY(int idx);
|
||||
|
||||
/**
|
||||
* @return The z-position of the vertex at index {@param idx}
|
||||
*/
|
||||
float getZ(int idx);
|
||||
|
||||
/**
|
||||
* @return The integer-encoded color of the vertex at index {@param idx}
|
||||
*/
|
||||
int getColor(int idx);
|
||||
|
||||
/**
|
||||
* @return The texture x-coordinate for the vertex at index {@param idx}
|
||||
*/
|
||||
float getTexU(int idx);
|
||||
|
||||
/**
|
||||
* @return The texture y-coordinate for the vertex at index {@param idx}
|
||||
*/
|
||||
float getTexV(int idx);
|
||||
|
||||
/**
|
||||
* @return The packed lightmap coordinates for the vertex at index {@param idx}, or zero if regular lighting should
|
||||
* be used.
|
||||
*/
|
||||
int getLight(int idx);
|
||||
|
||||
/**
|
||||
* @return The integer bit flags containing the {@link ModelQuadFlags} for this quad
|
||||
*/
|
||||
int getFlags();
|
||||
|
||||
/**
|
||||
* @return The color index of this quad.
|
||||
*/
|
||||
int getColorIndex();
|
||||
|
||||
/**
|
||||
* @return The sprite texture used by this quad, or null if none is attached
|
||||
*/
|
||||
Object celeritas$getSprite();
|
||||
|
||||
/**
|
||||
* @return The face used by this quad for lighting effects
|
||||
*/
|
||||
ModelQuadFacing getLightFace();
|
||||
|
||||
/**
|
||||
* @return The direction in which this quad is pointing, based on its geometry
|
||||
*/
|
||||
ModelQuadFacing getNormalFace();
|
||||
|
||||
int getForgeNormal(int idx);
|
||||
|
||||
int getComputedFaceNormal();
|
||||
|
||||
/**
|
||||
* {@return the normal of vertex 0, if specified, or Embeddium's autogenerated face normal otherwise}
|
||||
*/
|
||||
default int getModFaceNormal() {
|
||||
int normal = getForgeNormal(0);
|
||||
return normal != 0 ? normal : getComputedFaceNormal();
|
||||
}
|
||||
|
||||
default boolean hasColor() {
|
||||
return this.getColorIndex() != -1;
|
||||
}
|
||||
|
||||
default boolean hasAmbientOcclusion() {
|
||||
return true;
|
||||
}
|
||||
|
||||
default int getVanillaLightEmission() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int getShaderBlockId();
|
||||
}
|
||||
+15
-33
@@ -1,9 +1,7 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad;
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad;
|
||||
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.properties.ModelQuadFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFlags;
|
||||
|
||||
/**
|
||||
* Provides a mutable view to a model quad.
|
||||
@@ -30,16 +28,6 @@ public interface ModelQuadViewMutable extends ModelQuadView {
|
||||
*/
|
||||
void setColor(int idx, int color);
|
||||
|
||||
/**
|
||||
* Convenience: set vertex color for all vertices at once.
|
||||
*/
|
||||
default void setColors(int abgr) {
|
||||
this.setColor(0, abgr);
|
||||
this.setColor(1, abgr);
|
||||
this.setColor(2, abgr);
|
||||
this.setColor(3, abgr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the texture x-coordinate of the vertex at index {@param idx} to the value {@param u}
|
||||
*/
|
||||
@@ -55,11 +43,6 @@ public interface ModelQuadViewMutable extends ModelQuadView {
|
||||
*/
|
||||
void setLight(int idx, int light);
|
||||
|
||||
/**
|
||||
* Sets the integer-encoded normal vector of the vertex at index {@param idx} to the value {@param light}
|
||||
*/
|
||||
void setNormal(int idx, int norm);
|
||||
|
||||
/**
|
||||
* Sets the bit-flag field which contains the {@link ModelQuadFlags} for this quad
|
||||
*/
|
||||
@@ -68,7 +51,7 @@ public interface ModelQuadViewMutable extends ModelQuadView {
|
||||
/**
|
||||
* Sets the sprite used by this quad
|
||||
*/
|
||||
void setSprite(TextureAtlasSprite sprite);
|
||||
void setSprite(Object sprite);
|
||||
|
||||
/**
|
||||
* Sets the color index used by this quad
|
||||
@@ -76,17 +59,16 @@ public interface ModelQuadViewMutable extends ModelQuadView {
|
||||
void setColorIndex(int index);
|
||||
|
||||
/**
|
||||
* If not {@link ForgeDirection#UNKNOWN}, quad is coplanar with a block face which, if known, simplifies or
|
||||
* shortcuts geometric analysis that might otherwise be needed. Set to {@link ForgeDirection#UNKNOWN} if quad is not
|
||||
* coplanar or if this is not known. Also controls face culling during block rendering.
|
||||
*
|
||||
* <p>
|
||||
* {@link ForgeDirection#UNKNOWN} by default.
|
||||
*
|
||||
* <p>
|
||||
* This is different from the value reported by {@link Quad#getLightFace()}. That value is computed based on face
|
||||
* geometry and must be non-{@link ForgeDirection#UNKNOWN} in vanilla quads. That computed value is returned by
|
||||
* {@link #getLightFace()}.
|
||||
* Sets the face used by this quad for lighting effects
|
||||
*/
|
||||
void setCullFace(ForgeDirection dir);
|
||||
void setLightFace(ModelQuadFacing face);
|
||||
|
||||
void setHasAmbientOcclusion(boolean hasAmbientOcclusion);
|
||||
|
||||
/**
|
||||
* Sets the packed normal vector embedded in the vertex data.
|
||||
*/
|
||||
void setForgeNormal(int idx, int normal);
|
||||
|
||||
void setShaderBlockId(int shaderBlockId);
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util.NormI8;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public enum ModelQuadFacing {
|
||||
|
||||
POS_X(1, 0, 0, Axis.X),
|
||||
POS_Y(0, 1, 0, Axis.Y),
|
||||
POS_Z(0, 0, 1, Axis.Z),
|
||||
NEG_X(-1, 0, 0, Axis.X),
|
||||
NEG_Y(0, -1, 0, Axis.Y),
|
||||
NEG_Z(0, 0, -1, Axis.Z),
|
||||
UNASSIGNED(0, 0, 0, null);
|
||||
|
||||
public static final ModelQuadFacing[] VALUES = ModelQuadFacing.values();
|
||||
public static final ModelQuadFacing[] DIRECTIONS = Arrays.stream(VALUES).filter(facing -> facing != UNASSIGNED)
|
||||
.toArray(ModelQuadFacing[]::new);
|
||||
public static final ModelQuadFacing[] HORIZONTAL_DIRECTIONS = Arrays.stream(DIRECTIONS)
|
||||
.filter(facing -> facing.axis != Axis.Y).toArray(ModelQuadFacing[]::new);
|
||||
public static final Axis[] AXES = Axis.values();
|
||||
|
||||
public static final int COUNT = VALUES.length;
|
||||
|
||||
public static final int NONE = 0;
|
||||
public static final int ALL = (1 << COUNT) - 1;
|
||||
|
||||
@Getter
|
||||
private final int packedNormal;
|
||||
|
||||
@Getter
|
||||
private final int stepX, stepY, stepZ;
|
||||
|
||||
@Getter
|
||||
private final Axis axis;
|
||||
|
||||
ModelQuadFacing(int stepX, int stepY, int stepZ, Axis axis) {
|
||||
this.stepX = stepX;
|
||||
this.stepY = stepY;
|
||||
this.stepZ = stepZ;
|
||||
this.axis = axis;
|
||||
this.packedNormal = NormI8.pack(stepX, stepY, stepZ);
|
||||
}
|
||||
|
||||
public ModelQuadFacing getOpposite() {
|
||||
return switch (this) {
|
||||
case POS_Y -> NEG_Y;
|
||||
case NEG_Y -> POS_Y;
|
||||
case POS_X -> NEG_X;
|
||||
case NEG_X -> POS_X;
|
||||
case POS_Z -> NEG_Z;
|
||||
case NEG_Z -> POS_Z;
|
||||
default -> UNASSIGNED;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean isDirection() {
|
||||
return this != UNASSIGNED;
|
||||
}
|
||||
|
||||
public enum Axis {
|
||||
|
||||
X,
|
||||
Y,
|
||||
Z;
|
||||
|
||||
public ModelQuadFacing getFacing(boolean positive) {
|
||||
return switch (this) {
|
||||
case X -> positive ? POS_X : NEG_X;
|
||||
case Y -> positive ? POS_Y : NEG_Y;
|
||||
case Z -> positive ? POS_Z : NEG_Z;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util.ColorABGR;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.util.MathUtil;
|
||||
|
||||
public class ModelQuadFlags {
|
||||
|
||||
/**
|
||||
* Indicates that the quad does not fully cover the given face for the model.
|
||||
*/
|
||||
public static final int IS_PARTIAL = 0b001;
|
||||
|
||||
/**
|
||||
* Indicates that the quad is parallel to its light face.
|
||||
*/
|
||||
public static final int IS_PARALLEL = 0b010;
|
||||
|
||||
/**
|
||||
* Indicates that the quad is aligned to the block grid. This flag is only set if {@link #IS_PARALLEL} is set.
|
||||
*/
|
||||
public static final int IS_ALIGNED = 0b100;
|
||||
|
||||
/**
|
||||
* Indicates that the quad should be shaded using vanilla's getShade logic and the light face, rather than the
|
||||
* normals of each vertex.
|
||||
*/
|
||||
public static final int IS_VANILLA_SHADED = 0b1000;
|
||||
/**
|
||||
* Indicates that the particle sprite on this quad can be trusted to be the only sprite it shows.
|
||||
*/
|
||||
public static final int IS_TRUSTED_SPRITE = (1 << 4);
|
||||
/**
|
||||
* Indicates that this quad can use a more optimal terrain render pass based on its sprite.
|
||||
*/
|
||||
public static final int IS_PASS_OPTIMIZABLE = (1 << 5);
|
||||
/**
|
||||
* Indicates that the flags are populated for the quad.
|
||||
*/
|
||||
public static final int IS_POPULATED = (1 << 31);
|
||||
|
||||
/**
|
||||
* @return True if the bit-flag of {@link ModelQuadFlags} contains the given flag
|
||||
*/
|
||||
public static boolean contains(int flags, int mask) {
|
||||
return (flags & mask) != 0;
|
||||
}
|
||||
|
||||
public static int getQuadFlags(ModelQuadView quad, ModelQuadFacing face) {
|
||||
return getQuadFlags(quad, face, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a quad's vertex ordering matches Minecraft's canonical baked order for the given face.
|
||||
* <p>
|
||||
* Minecraft allows four different permutations of a quad's vertices that will render identically. However, the
|
||||
* baked model pipeline (e.g. ambient occlusion, lighting interpolation, and back-face culling) assumes a specific
|
||||
* ordering that is hardcoded in the model baking logic. This method verifies that a quad's vertices are listed in
|
||||
* that exact canonical order.
|
||||
* <p>
|
||||
* The canonical order is:
|
||||
* <ul>
|
||||
* <li>Vertices are arranged counter-clockwise (CCW) as seen from outside the block face.</li>
|
||||
* <li>Each face (±X, ±Y, ±Z) starts at a specific corner and proceeds CCW around the face.</li>
|
||||
* <li>For example, the -Y face (DOWN) starts at {@code (minX, minY, maxZ)} and proceeds through
|
||||
* {@code (minX, minY, minZ)}, {@code (maxX, minY, minZ)}, {@code (maxX, minY, maxZ)}.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* This method avoids allocations by comparing directly against the expected coordinates for each vertex index based
|
||||
* on the given {@code face} and bounding box.
|
||||
*
|
||||
* @param face the face direction of the quad; determines which axis is fixed and which two axes form the quad plane
|
||||
* @return {@code true} if the quad's vertices match Minecraft's canonical baked order for the given face,
|
||||
* {@code false} otherwise
|
||||
*/
|
||||
private static boolean canonicalVertexOrder(ModelQuadView quad, ModelQuadFacing face, float minX, float minY,
|
||||
float minZ, float maxX, float maxY, float maxZ) {
|
||||
return switch (face) {
|
||||
case NEG_Y -> MathUtil.roughlyEqual(quad.getX(0), minX) && MathUtil.roughlyEqual(quad.getY(0), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(0), maxZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(1), minX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(1), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(1), minZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(2), maxX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(2), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(2), minZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(3), maxX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(3), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(3), maxZ);
|
||||
case POS_Y -> MathUtil.roughlyEqual(quad.getX(0), minX) && MathUtil.roughlyEqual(quad.getY(0), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(0), minZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(1), minX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(1), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(1), maxZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(2), maxX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(2), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(2), maxZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(3), maxX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(3), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(3), minZ);
|
||||
case NEG_Z -> MathUtil.roughlyEqual(quad.getX(0), maxX) && MathUtil.roughlyEqual(quad.getY(0), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(0), minZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(1), maxX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(1), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(1), minZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(2), minX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(2), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(2), minZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(3), minX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(3), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(3), minZ);
|
||||
case POS_Z -> MathUtil.roughlyEqual(quad.getX(0), minX) && MathUtil.roughlyEqual(quad.getY(0), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(0), maxZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(1), minX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(1), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(1), maxZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(2), maxX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(2), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(2), maxZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(3), maxX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(3), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(3), maxZ);
|
||||
case NEG_X -> MathUtil.roughlyEqual(quad.getX(0), minX) && MathUtil.roughlyEqual(quad.getY(0), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(0), minZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(1), minX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(1), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(1), minZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(2), minX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(2), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(2), maxZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(3), minX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(3), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(3), maxZ);
|
||||
case POS_X -> MathUtil.roughlyEqual(quad.getX(0), maxX) && MathUtil.roughlyEqual(quad.getY(0), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(0), maxZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(1), maxX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(1), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(1), maxZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(2), maxX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(2), minY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(2), minZ)
|
||||
&& MathUtil.roughlyEqual(quad.getX(3), maxX)
|
||||
&& MathUtil.roughlyEqual(quad.getY(3), maxY)
|
||||
&& MathUtil.roughlyEqual(quad.getZ(3), minZ);
|
||||
case UNASSIGNED -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the properties of the given quad. This data is used later by the light pipeline in order to make
|
||||
* certain optimizations.
|
||||
*/
|
||||
public static int getQuadFlags(ModelQuadView quad, ModelQuadFacing face, int existingFlags) {
|
||||
float minX = 32.0F;
|
||||
float minY = 32.0F;
|
||||
float minZ = 32.0F;
|
||||
|
||||
float maxX = -32.0F;
|
||||
float maxY = -32.0F;
|
||||
float maxZ = -32.0F;
|
||||
|
||||
int numVertices = 4;
|
||||
|
||||
boolean degenerate = false, nonOpaqueColor = false;
|
||||
|
||||
for (int i = 0; i < numVertices; ++i) {
|
||||
float x = quad.getX(i);
|
||||
float y = quad.getY(i);
|
||||
float z = quad.getZ(i);
|
||||
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
minZ = Math.min(minZ, z);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
maxZ = Math.max(maxZ, z);
|
||||
|
||||
for (int j = 0; j < i; ++j) {
|
||||
float px = quad.getX(j);
|
||||
float py = quad.getY(j);
|
||||
float pz = quad.getZ(j);
|
||||
|
||||
if (MathUtil.roughlyEqual(px, x) && MathUtil.roughlyEqual(py, y) && MathUtil.roughlyEqual(pz, z)) {
|
||||
degenerate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ColorABGR.unpackAlpha(quad.getColor(i)) != 255) {
|
||||
nonOpaqueColor = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not set the partial flag if the vertices are not listed in the expected order. The optimization assumes
|
||||
// it can map directly onto corners in specific locations.
|
||||
boolean partial = degenerate || (switch (face.getAxis()) {
|
||||
case X -> minY >= 0.0001f || minZ >= 0.0001f || maxY <= 0.9999F || maxZ <= 0.9999F;
|
||||
case Y -> minX >= 0.0001f || minZ >= 0.0001f || maxX <= 0.9999F || maxZ <= 0.9999F;
|
||||
case Z -> minX >= 0.0001f || minY >= 0.0001f || maxX <= 0.9999F || maxY <= 0.9999F;
|
||||
}) || !canonicalVertexOrder(quad, face, minX, minY, minZ, maxX, maxY, maxZ);
|
||||
|
||||
boolean parallel = switch (face.getAxis()) {
|
||||
case X -> minX == maxX;
|
||||
case Y -> minY == maxY;
|
||||
case Z -> minZ == maxZ;
|
||||
};
|
||||
|
||||
boolean aligned = parallel && switch (face) {
|
||||
case NEG_Y -> minY < 0.0001f;
|
||||
case POS_Y -> maxY > 0.9999F;
|
||||
case NEG_Z -> minZ < 0.0001f;
|
||||
case POS_Z -> maxZ > 0.9999F;
|
||||
case NEG_X -> minX < 0.0001f;
|
||||
case POS_X -> maxX > 0.9999F;
|
||||
case UNASSIGNED -> throw new IllegalArgumentException();
|
||||
};
|
||||
|
||||
int flags = existingFlags & ~(IS_PARTIAL | IS_PARALLEL | IS_ALIGNED);
|
||||
|
||||
if (partial) {
|
||||
flags |= IS_PARTIAL;
|
||||
}
|
||||
|
||||
if (parallel) {
|
||||
flags |= IS_PARALLEL;
|
||||
}
|
||||
|
||||
if (aligned) {
|
||||
flags |= IS_ALIGNED;
|
||||
}
|
||||
|
||||
if (!nonOpaqueColor && (flags & IS_TRUSTED_SPRITE) != 0) {
|
||||
flags |= IS_PASS_OPTIMIZABLE;
|
||||
}
|
||||
|
||||
flags |= IS_POPULATED;
|
||||
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -1,4 +1,4 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad.properties;
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties;
|
||||
|
||||
/**
|
||||
* Defines the orientation of vertices in a model quad. This information be used to re-orient the quad's vertices to a
|
||||
@@ -25,9 +25,20 @@ public enum ModelQuadOrientation {
|
||||
/**
|
||||
* Determines the orientation of the vertices in the quad.
|
||||
*/
|
||||
public static ModelQuadOrientation orient(float[] brightnesses) {
|
||||
public static ModelQuadOrientation orientByBrightness(float[] brightnesses, int[] lightmaps) {
|
||||
// If one side of the quad is brighter, flip the sides
|
||||
if (brightnesses[0] + brightnesses[2] > brightnesses[1] + brightnesses[3]) {
|
||||
float br02 = brightnesses[0] + brightnesses[2];
|
||||
float br13 = brightnesses[1] + brightnesses[3];
|
||||
if (br02 > br13) {
|
||||
return NORMAL;
|
||||
} else if (br02 < br13) {
|
||||
return FLIP;
|
||||
}
|
||||
|
||||
// If one side of the quad is darker, flip the sides
|
||||
int lm02 = lightmaps[0] + lightmaps[2];
|
||||
int lm13 = lightmaps[1] + lightmaps[3];
|
||||
if (lm02 <= lm13) {
|
||||
return NORMAL;
|
||||
} else {
|
||||
return FLIP;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties;
|
||||
|
||||
public enum ModelQuadWinding {
|
||||
|
||||
CLOCKWISE(new int[] { 0, 1, 2, 2, 3, 0 }),
|
||||
COUNTERCLOCKWISE(new int[] { 0, 3, 2, 1, 0, 2 });
|
||||
|
||||
private final int[] indices;
|
||||
|
||||
ModelQuadWinding(int[] indices) {
|
||||
this.indices = indices;
|
||||
}
|
||||
|
||||
public int[] getIndices() {
|
||||
return this.indices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.polyfill;
|
||||
|
||||
import static it.unimi.dsi.fastutil.objects.Object2ObjectMaps.unmodifiable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
|
||||
public class Maps {
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public static <K, V> Object2ObjectMap<K, V> copyOf(Map<K, V> input) {
|
||||
if (input instanceof Object2ObjectMaps.UnmodifiableMap u) return u;
|
||||
return unmodifiable(new Object2ObjectOpenHashMap<>(input));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.util;
|
||||
|
||||
public class MathUtil {
|
||||
|
||||
public static boolean roughlyEqual(float a, float b) {
|
||||
return Math.abs(b - a) < 0.00001f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.cel.util;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.NEG_X;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.NEG_Y;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.NEG_Z;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.POS_X;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.POS_Y;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.POS_Z;
|
||||
import static java.lang.Math.abs;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util.NormI8;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
|
||||
|
||||
/// Provides some utilities and constants for interacting with vanilla's model quad vertex format.
|
||||
/// This is the vertex format used by Minecraft 7.10 for chunk meshes and model quads. Internally, it uses integer
|
||||
/// arrays for store baked quad data, and as such the following table provides both the byte and int indices.
|
||||
///
|
||||
/// | Byte Index | Integer Index | Name | Format | Fields |
|
||||
/// | ---------- | ------------- | ---- | ------ | ------ |
|
||||
/// | 0..11 | 0..2 | Position | 3 floats | x, y, z |
|
||||
/// | 12..19 | 3..4 | Block Texture | 2 floats | u, v |
|
||||
/// | 19..23 | 5 | Color | 4 unsigned bytes | a, b, g, r \[1] |
|
||||
/// | 24..27 | 6 | Normal | 3 unsigned bytes | x, y, z |
|
||||
/// | 28..31 | 7 | Light Texture | 2 shorts | u, v |
|
||||
///
|
||||
/// \[1]: ABGR on little-endian systems, RGBA on big-endian systems.
|
||||
public class ModelQuadUtil {
|
||||
|
||||
// Integer indices for vertex attributes, useful for accessing baked quad data
|
||||
public static final int POSITION_INDEX = 0, COLOR_INDEX = 5, TEXTURE_INDEX = 3, LIGHT_INDEX = 7, NORMAL_INDEX = 6;
|
||||
|
||||
// Size of vertex format in 4-byte integers
|
||||
public static final int VERTEX_SIZE = 8;
|
||||
|
||||
public static final int DEFAULT_COLOR = 0xFFFFFFFF;
|
||||
public static final int DEFAULT_LIGHTMAP = 15 << 20 | 15 << 4;
|
||||
|
||||
/// @param vertexIndex The index of the vertex to access
|
||||
/// @return The starting offset of the vertex's attributes
|
||||
public static int vertexOffset(int vertexIndex) {
|
||||
return vertexIndex * VERTEX_SIZE;
|
||||
}
|
||||
|
||||
public static ModelQuadFacing findNormalFace(float x, float y, float z) {
|
||||
if (!Float.isFinite(x) || !Float.isFinite(y) || !Float.isFinite(z)) {
|
||||
return ModelQuadFacing.UNASSIGNED;
|
||||
}
|
||||
|
||||
float maxDot = 0;
|
||||
ModelQuadFacing closestFace = null;
|
||||
|
||||
for (ModelQuadFacing face : ModelQuadFacing.DIRECTIONS) {
|
||||
float dot = (x * face.getStepX()) + (y * face.getStepY()) + (z * face.getStepZ());
|
||||
|
||||
if (dot > maxDot) {
|
||||
maxDot = dot;
|
||||
closestFace = face;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestFace != null && abs(maxDot - 1.0f) < 1.0E-5F) {
|
||||
return closestFace;
|
||||
}
|
||||
|
||||
return ModelQuadFacing.UNASSIGNED;
|
||||
}
|
||||
|
||||
public static ModelQuadFacing findNormalFace(int normal) {
|
||||
return findNormalFace(NormI8.unpackX(normal), NormI8.unpackY(normal), NormI8.unpackZ(normal));
|
||||
}
|
||||
|
||||
public static ModelQuadFacing findLightFace(int normal) {
|
||||
float x = NormI8.unpackX(normal);
|
||||
float y = NormI8.unpackY(normal);
|
||||
float z = NormI8.unpackZ(normal);
|
||||
var max = (max(max((abs(x)), abs(y)), abs(z)));
|
||||
|
||||
if (max == x) return x < 0 ? NEG_X : POS_X;
|
||||
if (max == z) return z < 0 ? NEG_Z : POS_Z;
|
||||
return y < 0 ? NEG_Y : POS_Y;
|
||||
}
|
||||
|
||||
public static int calculateNormal(ModelQuadView quad) {
|
||||
final float x0 = quad.getX(0);
|
||||
final float y0 = quad.getY(0);
|
||||
final float z0 = quad.getZ(0);
|
||||
|
||||
final float x1 = quad.getX(1);
|
||||
final float y1 = quad.getY(1);
|
||||
final float z1 = quad.getZ(1);
|
||||
|
||||
final float x2 = quad.getX(2);
|
||||
final float y2 = quad.getY(2);
|
||||
final float z2 = quad.getZ(2);
|
||||
|
||||
final float x3 = quad.getX(3);
|
||||
final float y3 = quad.getY(3);
|
||||
final float z3 = quad.getZ(3);
|
||||
|
||||
final float dx0 = x2 - x0;
|
||||
final float dy0 = y2 - y0;
|
||||
final float dz0 = z2 - z0;
|
||||
final float dx1 = x3 - x1;
|
||||
final float dy1 = y3 - y1;
|
||||
final float dz1 = z3 - z1;
|
||||
|
||||
float normX = dy0 * dz1 - dz0 * dy1;
|
||||
float normY = dz0 * dx1 - dx0 * dz1;
|
||||
float normZ = dx0 * dy1 - dy0 * dx1;
|
||||
|
||||
float l = (float) Math.sqrt(normX * normX + normY * normY + normZ * normZ);
|
||||
|
||||
if (l != 0) {
|
||||
normX /= l;
|
||||
normY /= l;
|
||||
normZ /= l;
|
||||
}
|
||||
|
||||
return NormI8.pack(normX, normY, normZ);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static int mergeBakedLight(int packedLight, int vanillaLightEmission, int calcLight) {
|
||||
// bail early in most cases
|
||||
if (packedLight == 0 && vanillaLightEmission == 0) return calcLight;
|
||||
|
||||
int psl = (packedLight >> 16) & 0xFF;
|
||||
int csl = (calcLight >> 16) & 0xFF;
|
||||
int pbl = (packedLight) & 0xFF;
|
||||
int cbl = (calcLight) & 0xFF;
|
||||
int bl = Math.max(Math.max(pbl, cbl), vanillaLightEmission);
|
||||
int sl = Math.max(Math.max(psl, csl), vanillaLightEmission);
|
||||
return (sl << 16) | bl;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad;
|
||||
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.properties.ModelQuadFacing;
|
||||
|
||||
public enum Axis {
|
||||
|
||||
X,
|
||||
Y,
|
||||
Z;
|
||||
|
||||
public static Axis fromDirection(ModelQuadFacing dir) {
|
||||
return switch (dir) {
|
||||
case DOWN, UP -> Y;
|
||||
case NORTH, SOUTH -> Z;
|
||||
case WEST, EAST -> X;
|
||||
case UNASSIGNED -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static Axis fromDirection(ForgeDirection dir) {
|
||||
return switch (dir) {
|
||||
case DOWN, UP -> Y;
|
||||
case NORTH, SOUTH -> Z;
|
||||
case WEST, EAST -> X;
|
||||
case UNKNOWN -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static Axis fromName(String dir) {
|
||||
return switch (dir) {
|
||||
case "y" -> Y;
|
||||
case "z" -> Z;
|
||||
case "x" -> X;
|
||||
default -> null;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
public enum Direction {
|
||||
|
||||
POSITIVE,
|
||||
NEGATIVE;
|
||||
|
||||
public static Direction fromDirection(ForgeDirection dir) {
|
||||
return switch (dir) {
|
||||
case DOWN, NORTH, WEST -> NEGATIVE;
|
||||
case UP, SOUTH, EAST -> POSITIVE;
|
||||
case UNKNOWN -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.util.ModelQuadUtil.COLOR_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.util.ModelQuadUtil.LIGHT_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.util.ModelQuadUtil.NORMAL_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.util.ModelQuadUtil.POSITION_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.util.ModelQuadUtil.TEXTURE_INDEX;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.util.ModelQuadUtil.VERTEX_SIZE;
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.util.ModelQuadUtil.vertexOffset;
|
||||
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
/**
|
||||
* A simple implementation of the {@link ModelQuadViewMutable} interface which can provide an on-heap scratch area for
|
||||
* storing quad vertex data.
|
||||
*/
|
||||
public class ModelQuad implements ModelQuadViewMutable {
|
||||
|
||||
private final int[] data = new int[VERTEX_SIZE * 4];
|
||||
private int flags;
|
||||
|
||||
private TextureAtlasSprite sprite;
|
||||
private int colorIdx;
|
||||
|
||||
@Override
|
||||
public void setX(int idx, float x) {
|
||||
this.data[vertexOffset(idx) + POSITION_INDEX] = Float.floatToRawIntBits(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setY(int idx, float y) {
|
||||
this.data[vertexOffset(idx) + POSITION_INDEX + 1] = Float.floatToRawIntBits(y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZ(int idx, float z) {
|
||||
this.data[vertexOffset(idx) + POSITION_INDEX + 2] = Float.floatToRawIntBits(z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColor(int idx, int color) {
|
||||
this.data[vertexOffset(idx) + COLOR_INDEX] = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTexU(int idx, float u) {
|
||||
this.data[vertexOffset(idx) + TEXTURE_INDEX] = Float.floatToRawIntBits(u);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTexV(int idx, float v) {
|
||||
this.data[vertexOffset(idx) + TEXTURE_INDEX + 1] = Float.floatToRawIntBits(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLight(int idx, int light) {
|
||||
this.data[vertexOffset(idx) + LIGHT_INDEX] = light;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNormal(int idx, int norm) {
|
||||
this.data[vertexOffset(idx) + NORMAL_INDEX] = norm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFlags(int flags) {
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSprite(TextureAtlasSprite sprite) {
|
||||
this.sprite = sprite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorIndex(int index) {
|
||||
this.colorIdx = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Doesn't do anything useful, but I'm not sure if it has to.
|
||||
*/
|
||||
@Override
|
||||
public void setCullFace(ForgeDirection f) {}
|
||||
|
||||
@Override
|
||||
public int getLight(int idx) {
|
||||
return this.data[vertexOffset(idx) + LIGHT_INDEX];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNormal(int idx) {
|
||||
return this.data[vertexOffset(idx) + NORMAL_INDEX];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColorIndex() {
|
||||
return this.colorIdx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Doesn't do anything useful, but I'm not sure if it has to.
|
||||
*/
|
||||
@Override
|
||||
public ForgeDirection getLightFace() {
|
||||
return ForgeDirection.UP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Doesn't do anything useful, but I'm not sure if it has to.
|
||||
*/
|
||||
@Override
|
||||
public ForgeDirection getCullFace() {
|
||||
return ForgeDirection.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getX(int idx) {
|
||||
return Float.intBitsToFloat(this.data[vertexOffset(idx) + POSITION_INDEX]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getY(int idx) {
|
||||
return Float.intBitsToFloat(this.data[vertexOffset(idx) + POSITION_INDEX + 1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getZ(int idx) {
|
||||
return Float.intBitsToFloat(this.data[vertexOffset(idx) + POSITION_INDEX + 2]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor(int idx) {
|
||||
if (vertexOffset(idx) + COLOR_INDEX < data.length) {
|
||||
return this.data[vertexOffset(idx) + COLOR_INDEX];
|
||||
} else {
|
||||
return data.length;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTexU(int idx) {
|
||||
return Float.intBitsToFloat(this.data[vertexOffset(idx) + TEXTURE_INDEX]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTexV(int idx) {
|
||||
return Float.intBitsToFloat(this.data[vertexOffset(idx) + TEXTURE_INDEX + 1]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFlags() {
|
||||
return this.flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextureAtlasSprite rubidium$getSprite() {
|
||||
return this.sprite;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.properties.ModelQuadFlags;
|
||||
|
||||
/**
|
||||
* Provides a read-only view of a model quad. For mutable access to a model quad, see {@link ModelQuadViewMutable}.
|
||||
*/
|
||||
public interface ModelQuadView {
|
||||
|
||||
/**
|
||||
* @return The x-position of the vertex at index {@param idx}
|
||||
*/
|
||||
float getX(int idx);
|
||||
|
||||
/**
|
||||
* @return The y-position of the vertex at index {@param idx}
|
||||
*/
|
||||
float getY(int idx);
|
||||
|
||||
/**
|
||||
* @return The z-position of the vertex at index {@param idx}
|
||||
*/
|
||||
float getZ(int idx);
|
||||
|
||||
/**
|
||||
* @return The integer-encoded color (ABGR?) of the vertex at index {@param idx}
|
||||
*/
|
||||
int getColor(int idx);
|
||||
|
||||
/**
|
||||
* @return The texture x-coordinate for the vertex at index {@param idx}
|
||||
*/
|
||||
float getTexU(int idx);
|
||||
|
||||
/**
|
||||
* @return The texture y-coordinate for the vertex at index {@param idx}
|
||||
*/
|
||||
float getTexV(int idx);
|
||||
|
||||
/**
|
||||
* @return The integer bit flags containing the {@link ModelQuadFlags} for this quad
|
||||
*/
|
||||
int getFlags();
|
||||
|
||||
/**
|
||||
* @return The lightmap texture coordinates for the vertex at index {@param idx}
|
||||
*/
|
||||
int getLight(int idx);
|
||||
|
||||
/**
|
||||
* @return The integer-encoded normal vector for the vertex at index {@param idx}
|
||||
*/
|
||||
int getNormal(int idx);
|
||||
|
||||
/**
|
||||
* @return The color index of this quad.
|
||||
*/
|
||||
int getColorIndex();
|
||||
|
||||
/**
|
||||
* If not {@link ForgeDirection#UNKNOWN}, quad should not be rendered in-world if the opposite face of a neighbor
|
||||
* block occludes it.
|
||||
*/
|
||||
ForgeDirection getCullFace();
|
||||
|
||||
/**
|
||||
* This is the face used for vanilla lighting calculations and will be the block face to which the quad is most
|
||||
* closely aligned. Always the same as cull face for quads that are on a block face, but never
|
||||
* {@link ForgeDirection#UNKNOWN} or null.
|
||||
*/
|
||||
ForgeDirection getLightFace();
|
||||
|
||||
/**
|
||||
* Copies this quad's data into the specified buffer starting at the given position.
|
||||
*
|
||||
* @param buf The buffer to write this quad's data to
|
||||
* @param position The starting byte index to write to
|
||||
*/
|
||||
default void copyInto(ByteBuffer buf, int position) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
buf.putFloat(position, this.getX(i));
|
||||
buf.putFloat(position + 4, this.getY(i));
|
||||
buf.putFloat(position + 8, this.getZ(i));
|
||||
buf.putInt(position + 12, this.getColor(i));
|
||||
buf.putFloat(position + 16, this.getTexU(i));
|
||||
buf.putFloat(position + 20, this.getTexV(i));
|
||||
buf.putInt(position + 24, this.getLight(i));
|
||||
|
||||
position += 28;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The sprite texture used by this quad, or null if none is attached
|
||||
*/
|
||||
TextureAtlasSprite rubidium$getSprite();
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.joml.Vector3f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.properties.ModelQuadFacing;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.properties.ModelQuadFlags;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Roughly equivalent to 16.5's BakedQuadView and BakedQuadMixin
|
||||
*/
|
||||
public class Quad implements QuadView {
|
||||
// Adapted from Neodymium
|
||||
|
||||
public static final int VERTEX_STRIDE = 8;
|
||||
public static final int QUAD_STRIDE = VERTEX_STRIDE * 4;
|
||||
public static final int X_INDEX = 0;
|
||||
public static final int Y_INDEX = 1;
|
||||
public static final int Z_INDEX = 2;
|
||||
public static final int U_INDEX = 3;
|
||||
public static final int V_INDEX = 4;
|
||||
public static final int COLOR_INDEX = 5;
|
||||
public static final int NORMAL_INDEX = 6;
|
||||
public static final int LIGHTMAP_INDEX = 7;
|
||||
|
||||
private final static int DEFAULT_LIGHTMAP = 15 << 20 | 15 << 4;
|
||||
private final static int DEFAULT_COLOR = 0xFFFFFFFF;
|
||||
|
||||
protected final int[] data = new int[QUAD_STRIDE];
|
||||
|
||||
@Getter
|
||||
private boolean deleted = false;
|
||||
|
||||
private final Vector3f vectorA = new Vector3f(), vectorB = new Vector3f(), vectorC = new Vector3f();
|
||||
|
||||
@Getter
|
||||
private boolean shade;
|
||||
private int cachedFlags;
|
||||
@Getter
|
||||
private ForgeDirection face;
|
||||
private int colorIndex = -1;
|
||||
private int shaderBlockId = -1;
|
||||
private TextureAtlasSprite sprite = null;
|
||||
|
||||
/** Returns the face, forced to take one of 6 directions to mirror the behavior of baked quads in 1.16.5. */
|
||||
@Override
|
||||
public ForgeDirection getLightFace() {
|
||||
return this.face != ForgeDirection.UNKNOWN ? this.face : ForgeDirection.UP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForgeDirection getCullFace() {
|
||||
return this.face;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getX(int idx) {
|
||||
return Float.intBitsToFloat(this.data[idx * VERTEX_STRIDE + X_INDEX]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getY(int idx) {
|
||||
return Float.intBitsToFloat(this.data[idx * VERTEX_STRIDE + Y_INDEX]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getZ(int idx) {
|
||||
return Float.intBitsToFloat(this.data[idx * VERTEX_STRIDE + Z_INDEX]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTexU(int idx) {
|
||||
return Float.intBitsToFloat(this.data[idx * VERTEX_STRIDE + U_INDEX]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTexV(int idx) {
|
||||
return Float.intBitsToFloat(this.data[idx * VERTEX_STRIDE + V_INDEX]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor(int idx) {
|
||||
return this.data[idx * VERTEX_STRIDE + COLOR_INDEX];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLight(int idx) {
|
||||
return this.data[idx * VERTEX_STRIDE + LIGHTMAP_INDEX];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNormal(int idx) {
|
||||
return this.data[idx * VERTEX_STRIDE + NORMAL_INDEX];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFlags() {
|
||||
return this.cachedFlags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColorIndex() {
|
||||
return this.colorIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getShaderBlockId() {
|
||||
return this.shaderBlockId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextureAtlasSprite rubidium$getSprite() {
|
||||
return this.sprite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCullFace(ForgeDirection face) {
|
||||
this.face = face;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setX(int idx, float x) {
|
||||
this.data[idx * VERTEX_STRIDE + X_INDEX] = Float.floatToRawIntBits(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setY(int idx, float y) {
|
||||
this.data[idx * VERTEX_STRIDE + Y_INDEX] = Float.floatToRawIntBits(y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZ(int idx, float z) {
|
||||
this.data[idx * VERTEX_STRIDE + Z_INDEX] = Float.floatToRawIntBits(z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTexU(int idx, float u) {
|
||||
this.data[idx * VERTEX_STRIDE + U_INDEX] = Float.floatToRawIntBits(u);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTexV(int idx, float v) {
|
||||
this.data[idx * VERTEX_STRIDE + V_INDEX] = Float.floatToRawIntBits(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColor(int idx, int c) {
|
||||
this.data[idx * VERTEX_STRIDE + COLOR_INDEX] = c;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLight(int idx, int l) {
|
||||
this.data[idx * VERTEX_STRIDE + LIGHTMAP_INDEX] = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNormal(int idx, int n) {
|
||||
this.data[idx * VERTEX_STRIDE + NORMAL_INDEX] = n;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFlags(int flags) {
|
||||
this.cachedFlags = flags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorIndex(int i) {
|
||||
this.colorIndex = i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSprite(TextureAtlasSprite sprite) {
|
||||
this.sprite = sprite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShaderBlockId(int shaderBlockId) {
|
||||
this.shaderBlockId = shaderBlockId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getRawData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: access x, y, z by index 0-2.
|
||||
*/
|
||||
private float posByIndex(int vertexIndex, int coordinateIndex) {
|
||||
return Float.intBitsToFloat(this.data[vertexIndex * VERTEX_STRIDE + coordinateIndex]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: access x, y, z by index 0-2.
|
||||
*/
|
||||
private void setPosByIndex(int vertexIndex, int coordinateIndex, float val) {
|
||||
this.data[vertexIndex * VERTEX_STRIDE + coordinateIndex] = Float.floatToRawIntBits(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset all xs, ys, or zs by index 0-2.
|
||||
*/
|
||||
private void offsetPos(int idx, float offset) {
|
||||
this.setPosByIndex(0, idx, this.posByIndex(0, idx) + offset);
|
||||
this.setPosByIndex(1, idx, this.posByIndex(1, idx) + offset);
|
||||
this.setPosByIndex(2, idx, this.posByIndex(2, idx) + offset);
|
||||
this.setPosByIndex(3, idx, this.posByIndex(3, idx) + offset);
|
||||
}
|
||||
|
||||
private void clearNormals() {
|
||||
this.setNormal(0, 0);
|
||||
this.setNormal(1, 0);
|
||||
this.setNormal(2, 0);
|
||||
this.setNormal(3, 0);
|
||||
}
|
||||
|
||||
private void clearLight() {
|
||||
this.setLight(0, DEFAULT_LIGHTMAP);
|
||||
this.setLight(1, DEFAULT_LIGHTMAP);
|
||||
this.setLight(2, DEFAULT_LIGHTMAP);
|
||||
this.setLight(3, DEFAULT_LIGHTMAP);
|
||||
}
|
||||
|
||||
private boolean isEmpty() {
|
||||
return this.getX(0) == this.getX(1) && this.getX(1) == this.getX(2)
|
||||
&& this.getX(2) == this.getX(3)
|
||||
&& this.getY(0) == this.getY(1)
|
||||
&& this.getY(1) == this.getY(2)
|
||||
&& this.getY(2) == this.getY(3);
|
||||
}
|
||||
|
||||
private void calcNormal() {
|
||||
this.vectorA.set(this.getX(1) - this.getX(0), this.getY(1) - this.getY(0), this.getZ(1) - this.getZ(0));
|
||||
this.vectorB.set(this.getX(2) - this.getX(1), this.getY(2) - this.getY(1), this.getZ(2) - this.getZ(1));
|
||||
this.vectorA.cross(this.vectorB, this.vectorC);
|
||||
}
|
||||
|
||||
protected void quadrangulate() {
|
||||
this.setX(3, this.getX(2));
|
||||
this.setY(3, this.getY(2));
|
||||
this.setZ(3, this.getZ(2));
|
||||
|
||||
this.setTexU(3, this.getTexU(2));
|
||||
this.setTexV(3, this.getTexV(2));
|
||||
|
||||
this.setLight(3, this.getLight(2));
|
||||
this.setColor(3, this.getColor(2));
|
||||
this.setNormal(3, this.getNormal(2));
|
||||
}
|
||||
|
||||
@Override
|
||||
public QuadView copyFrom(QuadView quad) {
|
||||
|
||||
System.arraycopy(quad.getRawData(), 0, this.data, 0, QUAD_STRIDE);
|
||||
|
||||
this.deleted = quad.isDeleted();
|
||||
this.shade = quad.isShade();
|
||||
this.face = quad.getFace();
|
||||
this.colorIndex = quad.getColorIndex();
|
||||
this.cachedFlags = quad.getFlags();
|
||||
this.sprite = quad.rubidium$getSprite();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void read(int[] rawBuffer, int offset, float offsetX, float offsetY, float offsetZ, int drawMode,
|
||||
Quad.Flags flags) {
|
||||
System.arraycopy(rawBuffer, offset, this.data, 0, QUAD_STRIDE);
|
||||
|
||||
if (offsetX != 0) this.offsetPos(0, offsetX);
|
||||
if (offsetY != 0) this.offsetPos(1, offsetY);
|
||||
if (offsetZ != 0) this.offsetPos(2, offsetZ);
|
||||
|
||||
if (!flags.hasColor) this.setColors(DEFAULT_COLOR);
|
||||
if (!flags.hasNormals) this.clearNormals();
|
||||
if (!flags.hasBrightness) this.clearLight();
|
||||
|
||||
// sus
|
||||
this.shade = flags.hasBrightness;
|
||||
|
||||
if (drawMode == GL11.GL_TRIANGLES) this.quadrangulate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setState(int[] rawBuffer, int offset, Quad.Flags flags, int drawMode, float offsetX, float offsetY,
|
||||
float offsetZ) {
|
||||
this.deleted = false;
|
||||
|
||||
read(rawBuffer, offset, offsetX, offsetY, offsetZ, drawMode, flags);
|
||||
|
||||
if (this.isEmpty()) {
|
||||
// ignore empty quads (e.g. alpha pass of EnderIO item conduits)
|
||||
this.deleted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.calcNormal();
|
||||
this.face = ModelQuadFacing.toDirection(ModelQuadFacing.fromVector(this.vectorC));
|
||||
this.cachedFlags = ModelQuadFlags.getQuadFlags(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
Locale.ENGLISH,
|
||||
"%s[(%.1f, %.1f, %.1f), (%.1f, %.1f, %.1f), (%.1f, %.1f, %.1f), (%.1f, %.1f, %.1f)]",
|
||||
this.deleted ? "XXX " : "",
|
||||
this.getX(0),
|
||||
this.getY(0),
|
||||
this.getZ(0),
|
||||
this.getX(1),
|
||||
this.getY(1),
|
||||
this.getZ(1),
|
||||
this.getX(2),
|
||||
this.getY(2),
|
||||
this.getZ(2),
|
||||
this.getX(3),
|
||||
this.getY(3),
|
||||
this.getZ(3));
|
||||
}
|
||||
|
||||
public static class Flags {
|
||||
|
||||
boolean hasTexture;
|
||||
public boolean hasBrightness;
|
||||
public boolean hasColor;
|
||||
public boolean hasNormals;
|
||||
|
||||
public Flags(byte flags) {
|
||||
hasTexture = (flags & 1) != 0;
|
||||
hasBrightness = (flags & 2) != 0;
|
||||
hasColor = (flags & 4) != 0;
|
||||
hasNormals = (flags & 8) != 0;
|
||||
}
|
||||
|
||||
public Flags(boolean hasTexture, boolean hasBrightness, boolean hasColor, boolean hasNormals) {
|
||||
this.hasTexture = hasTexture;
|
||||
this.hasBrightness = hasBrightness;
|
||||
this.hasColor = hasColor;
|
||||
this.hasNormals = hasNormals;
|
||||
}
|
||||
|
||||
public byte toByte() {
|
||||
byte flags = 0;
|
||||
if (hasTexture) {
|
||||
flags |= 1;
|
||||
}
|
||||
if (hasBrightness) {
|
||||
flags |= 2;
|
||||
}
|
||||
if (hasColor) {
|
||||
flags |= 4;
|
||||
}
|
||||
if (hasNormals) {
|
||||
flags |= 8;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad;
|
||||
|
||||
import net.minecraft.util.IIcon;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.NdQuadBuilder;
|
||||
|
||||
public interface QuadBuilder {
|
||||
|
||||
/**
|
||||
* Causes texture to appear with no rotation. Pass in bakeFlags parameter to {@link #spriteBake(IIcon, int)}.
|
||||
*/
|
||||
int BAKE_ROTATE_NONE = 0;
|
||||
|
||||
/**
|
||||
* Causes texture to appear rotated 90 deg. clockwise relative to nominal face. Pass in bakeFlags parameter to
|
||||
* {@link #spriteBake(IIcon, int)}.
|
||||
*/
|
||||
int BAKE_ROTATE_90 = 1;
|
||||
|
||||
/**
|
||||
* Causes texture to appear rotated 180 deg. relative to nominal face. Pass in bakeFlags parameter to
|
||||
* {@link #spriteBake(IIcon, int)}.
|
||||
*/
|
||||
int BAKE_ROTATE_180 = 2;
|
||||
|
||||
/**
|
||||
* Causes texture to appear rotated 270 deg. clockwise relative to nominal face. Pass in bakeFlags parameter to
|
||||
* {@link #spriteBake(IIcon, int)}.
|
||||
*/
|
||||
int BAKE_ROTATE_270 = 3;
|
||||
|
||||
/**
|
||||
* When enabled, texture coordinate are assigned based on vertex position. Any existing UV coordinates will be
|
||||
* replaced. Pass in bakeFlags parameter to {@link #spriteBake(IIcon, int)}.
|
||||
*
|
||||
* <p>
|
||||
* UV lock always derives texture coordinates based on nominal face, even when the quad is not co-planar with that
|
||||
* face, and the result is the same as if the quad were projected onto the nominal face, which is usually the
|
||||
* desired result.
|
||||
*/
|
||||
int BAKE_LOCK_UV = 4;
|
||||
|
||||
/**
|
||||
* When set, U texture coordinates for the given sprite are flipped as part of baking. Can be useful for some
|
||||
* randomization and texture mapping scenarios. Results are different from what can be obtained via rotation and
|
||||
* both can be applied. Pass in bakeFlags parameter to {@link #spriteBake(IIcon, int)}.
|
||||
*/
|
||||
int BAKE_FLIP_U = 8;
|
||||
|
||||
/**
|
||||
* Same as {@link #BAKE_FLIP_U} but for V coordinate.
|
||||
*/
|
||||
int BAKE_FLIP_V = 16;
|
||||
|
||||
/**
|
||||
* UV coordinates by default are assumed to be 0-16 scale for consistency with conventional Minecraft model format.
|
||||
* This is scaled to 0-1 during baking before interpolation. Model loaders that already have 0-1 coordinates can
|
||||
* avoid wasteful multiplication/division by passing 0-1 coordinates directly. Pass in bakeFlags parameter to
|
||||
* {@link #spriteBake(IIcon, int)}.
|
||||
*/
|
||||
int BAKE_NORMALIZED = 32;
|
||||
|
||||
/**
|
||||
* Tolerance for determining if the depth parameter to
|
||||
* {@link #square(ForgeDirection, float, float, float, float, float)} is effectively zero - meaning the face is a
|
||||
* cull face.
|
||||
*/
|
||||
float CULL_FACE_EPSILON = 0.00001f;
|
||||
|
||||
static QuadBuilder getBuilder() {
|
||||
return new NdQuadBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies data to out, returns it, and resets builder for further use.
|
||||
*/
|
||||
QuadView build(QuadView out);
|
||||
|
||||
/**
|
||||
* See {@link #build(QuadView)}. This also rotates the output by the given matrix.
|
||||
*/
|
||||
QuadView build(QuadView out, Matrix4f rotMat);
|
||||
|
||||
/**
|
||||
* Tries to automatically set the culling and nominal face from the quad's geometry.
|
||||
*/
|
||||
void setCullFace();
|
||||
|
||||
/**
|
||||
* Provides a hint to renderer about the facing of this quad. Not required, but if provided can shortcut some
|
||||
* geometric analysis if the quad is parallel to a block face. Should be the expected value of
|
||||
* {@link ModelQuadView#getLightFace()}. Value will be confirmed and if invalid the correct light face will be
|
||||
* calculated.
|
||||
*
|
||||
* <p>
|
||||
* Null by default, and set automatically by {@link ModelQuadViewMutable#setCullFace(ForgeDirection)}.
|
||||
*
|
||||
* <p>
|
||||
* Models may also find this useful as the face for texture UV locking and rotation semantics.
|
||||
*
|
||||
* <p>
|
||||
* Note: This value is not persisted independently when the quad is encoded. When reading encoded quads, this value
|
||||
* will always be the same as {@link ModelQuadView#getLightFace()}.
|
||||
*/
|
||||
void nominalFace(@Nullable ForgeDirection face);
|
||||
|
||||
/**
|
||||
* See {@link #nominalFace(ForgeDirection)}
|
||||
*/
|
||||
ForgeDirection nominalFace();
|
||||
|
||||
/**
|
||||
* Sets the geometric vertex position for the given vertex, relative to block origin, (0,0,0). Minecraft rendering
|
||||
* is designed for models that fit within a single block space and is recommended that coordinates remain in the 0-1
|
||||
* range, with multi-block meshes split into multiple per-block models.
|
||||
*/
|
||||
void pos(int vertexIndex, float x, float y, float z);
|
||||
|
||||
/**
|
||||
* Convenience: set pos with a vector. See {@link #pos(int, float, float, float)}.
|
||||
*/
|
||||
void pos(int vertexIndex, Vector3f vec);
|
||||
|
||||
/**
|
||||
* Gets the vertex position as a vector. This allocates a new vector, do not use in dynamic rendering!
|
||||
*/
|
||||
Vector3f pos(int vertexIndex);
|
||||
|
||||
/**
|
||||
* Convenience: access x, y, z by index 0-2.
|
||||
*/
|
||||
float posByIndex(int vertexIndex, int coordinateIndex);
|
||||
|
||||
/**
|
||||
* Convienence, gets the icon from the block atlas. See {@link #spriteBake(IIcon, int)}
|
||||
*/
|
||||
void spriteBake(String spriteName, int bakeFlags);
|
||||
|
||||
/**
|
||||
* Assigns sprite atlas u,v coordinates to this quad for the given sprite. Can handle UV locking, rotation,
|
||||
* interpolation, etc. Control this behavior by passing additive combinations of the BAKE_ flags defined in this
|
||||
* interface.
|
||||
*/
|
||||
void spriteBake(IIcon sprite, int bakeFlags);
|
||||
|
||||
/**
|
||||
* Helper method to assign vertex coordinates for a square aligned with the given face. Ensures that vertex order is
|
||||
* consistent with vanilla convention. (Incorrect order can lead to bad AO lighting unless enhanced lighting logic
|
||||
* is available/enabled.)
|
||||
*
|
||||
* <p>
|
||||
* Square will be parallel to the given face and coplanar with the face (and culled if the face is occluded) if the
|
||||
* depth parameter is approximately zero. See {@link #CULL_FACE_EPSILON}.
|
||||
*
|
||||
* <p>
|
||||
* All coordinates should be normalized (0-1).
|
||||
*
|
||||
* <p>
|
||||
* The directions for the faces assume you're looking at them. For top and bottom, they assume you're looking north.
|
||||
*/
|
||||
void square(ForgeDirection nominalFace, float left, float bottom, float right, float top, float depth);
|
||||
|
||||
int tag();
|
||||
|
||||
/**
|
||||
* Encodes an integer tag with this quad that can later be retrieved via {@link #tag()}. Useful for models that want
|
||||
* to perform conditional transformation or filtering on static meshes.
|
||||
*/
|
||||
void tag(int tag);
|
||||
|
||||
/**
|
||||
* Set texture coordinates.
|
||||
*/
|
||||
void uv(int vertexIndex, float u, float v);
|
||||
|
||||
/**
|
||||
* Modern Minecraft uses magic arrays to do this without breaking AO. This is the same thing, but without arrays.
|
||||
*/
|
||||
static Vector3f mapSideToVertex(Vector3f from, Vector3f to, int index, ForgeDirection side) {
|
||||
|
||||
return switch (side) {
|
||||
case DOWN -> switch (index) {
|
||||
case 0 -> new Vector3f(from.x, from.y, to.z);
|
||||
case 1 -> new Vector3f(from.x, from.y, from.z);
|
||||
case 2 -> new Vector3f(to.x, from.y, from.z);
|
||||
case 3 -> new Vector3f(to.x, from.y, to.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case UP -> switch (index) {
|
||||
case 0 -> new Vector3f(from.x, to.y, from.z);
|
||||
case 1 -> new Vector3f(from.x, to.y, to.z);
|
||||
case 2 -> new Vector3f(to.x, to.y, to.z);
|
||||
case 3 -> new Vector3f(to.x, to.y, from.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case NORTH -> switch (index) {
|
||||
case 0 -> new Vector3f(to.x, to.y, from.z);
|
||||
case 1 -> new Vector3f(to.x, from.y, from.z);
|
||||
case 2 -> new Vector3f(from.x, from.y, from.z);
|
||||
case 3 -> new Vector3f(from.x, to.y, from.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case SOUTH -> switch (index) {
|
||||
case 0 -> new Vector3f(from.x, to.y, to.z);
|
||||
case 1 -> new Vector3f(from.x, from.y, to.z);
|
||||
case 2 -> new Vector3f(to.x, from.y, to.z);
|
||||
case 3 -> new Vector3f(to.x, to.y, to.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case WEST -> switch (index) {
|
||||
case 0 -> new Vector3f(from.x, to.y, from.z);
|
||||
case 1 -> new Vector3f(from.x, from.y, from.z);
|
||||
case 2 -> new Vector3f(from.x, from.y, to.z);
|
||||
case 3 -> new Vector3f(from.x, to.y, to.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case EAST -> switch (index) {
|
||||
case 0 -> new Vector3f(to.x, to.y, to.z);
|
||||
case 1 -> new Vector3f(to.x, from.y, to.z);
|
||||
case 2 -> new Vector3f(to.x, from.y, from.z);
|
||||
case 3 -> new Vector3f(to.x, to.y, from.z);
|
||||
default -> throw new RuntimeException("Too many indices!");
|
||||
};
|
||||
case UNKNOWN -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.IBlockPos;
|
||||
|
||||
public interface QuadProvider {
|
||||
|
||||
int R_MASK = 0xFF << 16;
|
||||
int G_MASK = 0xFF << 8;
|
||||
int B_MASK = 0xFF;
|
||||
|
||||
/**
|
||||
* Called once per block and returns the color packed as ABGR. If you want to vary colors within a block, just
|
||||
* ignore the color passed into {@link #getQuads} and use your own function, don't override this. But if you only
|
||||
* want to change color once per block, like vanilla leaves, override this. The default vanilla adapter is
|
||||
* {@link #getDefaultColor}
|
||||
*/
|
||||
default int getColor(IBlockAccess world, IBlockPos pos, Block block, int meta, Random random) {
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int getDefaultColor(IBlockAccess world, IBlockPos pos, Block block) {
|
||||
|
||||
final int cin = block.colorMultiplier(world, pos.getX(), pos.getY(), pos.getZ());
|
||||
return (0xFF << 24) | ((cin & B_MASK) << 16) | (cin & G_MASK) | ((cin & R_MASK) >>> 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* If you need to allocate new quads, set this to true. If true, the quads returned by {@link #getQuads} are
|
||||
* recycled, and you should not keep a reference to them. Example: stone can return a static list every time, but a
|
||||
* modded block which adds or removes quads based on location would need dynamic quads.
|
||||
*/
|
||||
default boolean isDynamic() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide quads to render. If you need new quads, they should be obtained with the passed supplier and
|
||||
* {@link #isDynamic} overridden to return true. If so, all quads in the list are recycled and references to them
|
||||
* should not be kept.
|
||||
*/
|
||||
List<QuadView> getQuads(IBlockAccess world, IBlockPos pos, Block block, int meta, ForgeDirection dir, Random random,
|
||||
int color, Supplier<QuadView> quadPool);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad;
|
||||
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
public interface QuadView extends ModelQuadViewMutable {
|
||||
|
||||
/**
|
||||
* Allocates a new quad - use sparingly, and not at all in render paths if you can help it. We don't need another
|
||||
* Malice Doors.
|
||||
*/
|
||||
static QuadView allocate() {
|
||||
return new Quad();
|
||||
}
|
||||
|
||||
boolean isShade();
|
||||
|
||||
boolean isDeleted();
|
||||
|
||||
ForgeDirection getFace();
|
||||
|
||||
QuadView copyFrom(QuadView src);
|
||||
|
||||
int[] getRawData();
|
||||
|
||||
void setShaderBlockId(int shaderBlockId);
|
||||
|
||||
int getShaderBlockId();
|
||||
|
||||
/**
|
||||
* Present for compatibility with the Tesselator, not recommended for general use.
|
||||
*/
|
||||
void setState(int[] rawBuffer, int offset, Quad.Flags flags, int drawMode, float offsetX, float offsetY,
|
||||
float offsetZ);
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad.properties;
|
||||
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.joml.Vector3f;
|
||||
|
||||
public enum ModelQuadFacing {
|
||||
|
||||
UP,
|
||||
DOWN,
|
||||
EAST,
|
||||
WEST,
|
||||
SOUTH,
|
||||
NORTH,
|
||||
UNASSIGNED;
|
||||
|
||||
public static final ModelQuadFacing[] VALUES = ModelQuadFacing.values();
|
||||
public static final int COUNT = VALUES.length;
|
||||
|
||||
public static ModelQuadFacing fromDirection(ForgeDirection dir) {
|
||||
return switch (dir) {
|
||||
case DOWN -> DOWN;
|
||||
case UP -> UP;
|
||||
case NORTH -> NORTH;
|
||||
case SOUTH -> SOUTH;
|
||||
case WEST -> WEST;
|
||||
case EAST -> EAST;
|
||||
default -> UNASSIGNED;
|
||||
};
|
||||
}
|
||||
|
||||
public static ForgeDirection toDirection(ModelQuadFacing dir) {
|
||||
return switch (dir) {
|
||||
case DOWN -> ForgeDirection.DOWN;
|
||||
case UP -> ForgeDirection.UP;
|
||||
case NORTH -> ForgeDirection.NORTH;
|
||||
case SOUTH -> ForgeDirection.SOUTH;
|
||||
case WEST -> ForgeDirection.WEST;
|
||||
case EAST -> ForgeDirection.EAST;
|
||||
default -> ForgeDirection.UNKNOWN;
|
||||
};
|
||||
}
|
||||
|
||||
public static ModelQuadFacing fromVector(Vector3f normal) {
|
||||
if (normal.x == 0f) {
|
||||
if (normal.y == 0f) {
|
||||
if (normal.z > 0) {
|
||||
return SOUTH;
|
||||
} else if (normal.z < 0) {
|
||||
return NORTH;
|
||||
}
|
||||
} else if (normal.z == 0f) {
|
||||
if (normal.y > 0) {
|
||||
return UP;
|
||||
} else if (normal.y < 0) {
|
||||
return DOWN;
|
||||
}
|
||||
}
|
||||
} else if (normal.y == 0f && (normal.z == 0f)) {
|
||||
if (normal.x > 0) {
|
||||
return EAST;
|
||||
} else if (normal.x < 0) {
|
||||
return WEST;
|
||||
}
|
||||
}
|
||||
return UNASSIGNED;
|
||||
}
|
||||
|
||||
public ModelQuadFacing getOpposite() {
|
||||
return switch (this) {
|
||||
case UP -> DOWN;
|
||||
case DOWN -> UP;
|
||||
case EAST -> WEST;
|
||||
case WEST -> EAST;
|
||||
case SOUTH -> NORTH;
|
||||
case NORTH -> SOUTH;
|
||||
default -> UNASSIGNED;
|
||||
};
|
||||
}
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad.properties;
|
||||
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.Axis;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.ModelQuadView;
|
||||
|
||||
public class ModelQuadFlags {
|
||||
|
||||
/**
|
||||
* Indicates that the quad does not fully cover the given face for the model.
|
||||
*/
|
||||
public static final int IS_PARTIAL = 0b001;
|
||||
|
||||
/**
|
||||
* Indicates that the quad is parallel to its light face.
|
||||
*/
|
||||
public static final int IS_PARALLEL = 0b010;
|
||||
|
||||
/**
|
||||
* Indicates that the quad is aligned to the block grid. This flag is only set if {@link #IS_PARALLEL} is set.
|
||||
*/
|
||||
public static final int IS_ALIGNED = 0b100;
|
||||
|
||||
/**
|
||||
* @return True if the bit-flag of {@link ModelQuadFlags} contains the given flag
|
||||
*/
|
||||
public static boolean contains(int flags, int mask) {
|
||||
return (flags & mask) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the properties of the given quad. This data is used later by the light pipeline in order to make
|
||||
* certain optimizations.
|
||||
*/
|
||||
public static int getQuadFlags(ModelQuadView quad) {
|
||||
final ForgeDirection face = quad.getLightFace();
|
||||
final Axis axis = Axis.fromDirection(ModelQuadFacing.fromDirection(face));
|
||||
|
||||
float minX = 32.0F;
|
||||
float minY = 32.0F;
|
||||
float minZ = 32.0F;
|
||||
|
||||
float maxX = -32.0F;
|
||||
float maxY = -32.0F;
|
||||
float maxZ = -32.0F;
|
||||
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
float x = quad.getX(i);
|
||||
float y = quad.getY(i);
|
||||
float z = quad.getZ(i);
|
||||
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
minZ = Math.min(minZ, z);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
maxZ = Math.max(maxZ, z);
|
||||
}
|
||||
|
||||
boolean partial = switch (axis) {
|
||||
case X -> minY >= 0.0001f || minZ >= 0.0001f || maxY <= 0.9999F || maxZ <= 0.9999F;
|
||||
case Y -> minX >= 0.0001f || minZ >= 0.0001f || maxX <= 0.9999F || maxZ <= 0.9999F;
|
||||
case Z -> minX >= 0.0001f || minY >= 0.0001f || maxX <= 0.9999F || maxY <= 0.9999F;
|
||||
};
|
||||
|
||||
boolean parallel = switch (axis) {
|
||||
case X -> minX == maxX;
|
||||
case Y -> minY == maxY;
|
||||
case Z -> minZ == maxZ;
|
||||
};
|
||||
|
||||
boolean flag = switch (face) {
|
||||
case DOWN -> minY < 0.0001f;
|
||||
case UP -> maxY > 0.9999F;
|
||||
case NORTH -> minZ < 0.0001f;
|
||||
case SOUTH -> maxZ > 0.9999F;
|
||||
case WEST -> minX < 0.0001f;
|
||||
case EAST -> maxX > 0.9999F;
|
||||
default -> false;
|
||||
};
|
||||
|
||||
boolean aligned = parallel && flag;
|
||||
|
||||
int flags = 0;
|
||||
|
||||
if (partial) {
|
||||
flags |= IS_PARTIAL;
|
||||
}
|
||||
|
||||
if (parallel) {
|
||||
flags |= IS_PARALLEL;
|
||||
}
|
||||
|
||||
if (aligned) {
|
||||
flags |= IS_ALIGNED;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.quad.writers;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
public class BufferWriter {
|
||||
// Adapted from Neodymium
|
||||
|
||||
private final ByteBuffer buf;
|
||||
|
||||
private final FloatBuffer floatBuffer;
|
||||
private final ShortBuffer shortBuffer;
|
||||
private final IntBuffer intBuffer;
|
||||
|
||||
public BufferWriter(ByteBuffer buf) {
|
||||
this.buf = buf;
|
||||
this.floatBuffer = buf.asFloatBuffer();
|
||||
this.shortBuffer = buf.asShortBuffer();
|
||||
this.intBuffer = buf.asIntBuffer();
|
||||
}
|
||||
|
||||
private void incrementPosition(int add) {
|
||||
buf.position(buf.position() + add);
|
||||
floatBuffer.position(buf.position() / 4);
|
||||
shortBuffer.position(buf.position() / 2);
|
||||
intBuffer.position(buf.position() / 4);
|
||||
}
|
||||
|
||||
public void writeFloat(float x) {
|
||||
try {
|
||||
floatBuffer.put(x);
|
||||
|
||||
incrementPosition(4);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void writeInt(int x) {
|
||||
intBuffer.put(x);
|
||||
|
||||
incrementPosition(4);
|
||||
}
|
||||
|
||||
public void writeByte(byte x) {
|
||||
buf.put(x); // this increments the buffer position by 1
|
||||
|
||||
incrementPosition(0);
|
||||
}
|
||||
|
||||
public int position() {
|
||||
return buf.position();
|
||||
}
|
||||
|
||||
public void writeShort(short s) {
|
||||
shortBuffer.put(s);
|
||||
|
||||
incrementPosition(2);
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -2,9 +2,9 @@ package com.gtnewhorizon.gtnhlib.client.renderer.quad.writers;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
|
||||
public interface IWriteQuads {
|
||||
|
||||
void writeQuad(QuadView quad, ByteBuffer buf);
|
||||
void writeQuad(ModelQuadView quad, ByteBuffer buf);
|
||||
}
|
||||
+5
-5
@@ -2,7 +2,7 @@ package com.gtnewhorizon.gtnhlib.client.renderer.quad.writers;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
|
||||
public class ItemVBOQuadWriter implements IWriteQuads {
|
||||
|
||||
@@ -18,7 +18,7 @@ public class ItemVBOQuadWriter implements IWriteQuads {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeQuad(QuadView quad, ByteBuffer buf) {
|
||||
public void writeQuad(ModelQuadView quad, ByteBuffer buf) {
|
||||
if (direct) {
|
||||
writeQuadDirect(quad, buf);
|
||||
} else {
|
||||
@@ -26,11 +26,11 @@ public class ItemVBOQuadWriter implements IWriteQuads {
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeQuadDirect(QuadView quad, ByteBuffer buf) {
|
||||
protected void writeQuadDirect(ModelQuadView quad, ByteBuffer buf) {
|
||||
throw new UnsupportedOperationException("Direct mode not supported yet");
|
||||
}
|
||||
|
||||
protected void writeQuadIndirect(QuadView quad, ByteBuffer buf) {
|
||||
protected void writeQuadIndirect(ModelQuadView quad, ByteBuffer buf) {
|
||||
for (int idx = 0; idx < 4; ++idx) {
|
||||
// Position
|
||||
buf.putFloat(quad.getX(idx));
|
||||
@@ -42,7 +42,7 @@ public class ItemVBOQuadWriter implements IWriteQuads {
|
||||
buf.putFloat(quad.getTexV(idx));
|
||||
|
||||
// Normals + Padding
|
||||
buf.putInt(quad.getNormal(idx));
|
||||
buf.putInt(quad.getForgeNormal(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -2,7 +2,7 @@ package com.gtnewhorizon.gtnhlib.client.renderer.quad.writers;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
|
||||
public class PositionColorTextureQuadWriter implements IWriteQuads {
|
||||
|
||||
@@ -18,7 +18,7 @@ public class PositionColorTextureQuadWriter implements IWriteQuads {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeQuad(QuadView quad, ByteBuffer buf) {
|
||||
public void writeQuad(ModelQuadView quad, ByteBuffer buf) {
|
||||
if (direct) {
|
||||
writeQuadDirect(quad, buf);
|
||||
} else {
|
||||
@@ -26,11 +26,11 @@ public class PositionColorTextureQuadWriter implements IWriteQuads {
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeQuadDirect(QuadView quad, ByteBuffer buf) {
|
||||
protected void writeQuadDirect(ModelQuadView quad, ByteBuffer buf) {
|
||||
throw new UnsupportedOperationException("Direct mode not supported yet");
|
||||
}
|
||||
|
||||
protected void writeQuadIndirect(QuadView quad, ByteBuffer buf) {
|
||||
protected void writeQuadIndirect(ModelQuadView quad, ByteBuffer buf) {
|
||||
for (int idx = 0; idx < 4; ++idx) {
|
||||
// Position
|
||||
buf.putFloat(quad.getX(idx));
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@ package com.gtnewhorizon.gtnhlib.client.renderer.quad.writers;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
|
||||
public class PositionQuadWriter implements IWriteQuads {
|
||||
|
||||
@@ -18,7 +18,7 @@ public class PositionQuadWriter implements IWriteQuads {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeQuad(QuadView quad, ByteBuffer buf) {
|
||||
public void writeQuad(ModelQuadView quad, ByteBuffer buf) {
|
||||
if (direct) {
|
||||
writeQuadDirect(quad, buf);
|
||||
} else {
|
||||
@@ -26,11 +26,11 @@ public class PositionQuadWriter implements IWriteQuads {
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeQuadDirect(QuadView quad, ByteBuffer buf) {
|
||||
protected void writeQuadDirect(ModelQuadView quad, ByteBuffer buf) {
|
||||
throw new UnsupportedOperationException("Direct mode not supported yet");
|
||||
}
|
||||
|
||||
protected void writeQuadIndirect(QuadView quad, ByteBuffer buf) {
|
||||
protected void writeQuadIndirect(ModelQuadView quad, ByteBuffer buf) {
|
||||
for (int idx = 0; idx < 4; ++idx) {
|
||||
// Position
|
||||
buf.putFloat(quad.getX(idx));
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@ package com.gtnewhorizon.gtnhlib.client.renderer.quad.writers;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
|
||||
public class PositionTextureColorQuadWriter implements IWriteQuads {
|
||||
|
||||
@@ -18,7 +18,7 @@ public class PositionTextureColorQuadWriter implements IWriteQuads {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeQuad(QuadView quad, ByteBuffer buf) {
|
||||
public void writeQuad(ModelQuadView quad, ByteBuffer buf) {
|
||||
if (direct) {
|
||||
writeQuadDirect(quad, buf);
|
||||
} else {
|
||||
@@ -26,11 +26,11 @@ public class PositionTextureColorQuadWriter implements IWriteQuads {
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeQuadDirect(QuadView quad, ByteBuffer buf) {
|
||||
protected void writeQuadDirect(ModelQuadView quad, ByteBuffer buf) {
|
||||
throw new UnsupportedOperationException("Direct mode not supported yet");
|
||||
}
|
||||
|
||||
protected void writeQuadIndirect(QuadView quad, ByteBuffer buf) {
|
||||
protected void writeQuadIndirect(ModelQuadView quad, ByteBuffer buf) {
|
||||
for (int idx = 0; idx < 4; ++idx) {
|
||||
// Position
|
||||
buf.putFloat(quad.getX(idx));
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@ package com.gtnewhorizon.gtnhlib.client.renderer.quad.writers;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
|
||||
public class PositionTextureQuadWriter implements IWriteQuads {
|
||||
|
||||
@@ -18,7 +18,7 @@ public class PositionTextureQuadWriter implements IWriteQuads {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeQuad(QuadView quad, ByteBuffer buf) {
|
||||
public void writeQuad(ModelQuadView quad, ByteBuffer buf) {
|
||||
if (direct) {
|
||||
writeQuadDirect(quad, buf);
|
||||
} else {
|
||||
@@ -26,11 +26,11 @@ public class PositionTextureQuadWriter implements IWriteQuads {
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeQuadDirect(QuadView quad, ByteBuffer buf) {
|
||||
protected void writeQuadDirect(ModelQuadView quad, ByteBuffer buf) {
|
||||
throw new UnsupportedOperationException("Direct mode not supported yet");
|
||||
}
|
||||
|
||||
protected void writeQuadIndirect(QuadView quad, ByteBuffer buf) {
|
||||
protected void writeQuadIndirect(ModelQuadView quad, ByteBuffer buf) {
|
||||
for (int idx = 0; idx < 4; ++idx) {
|
||||
// Position
|
||||
buf.putFloat(quad.getX(idx));
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.util;
|
||||
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector3i;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.properties.ModelQuadFacing;
|
||||
|
||||
/**
|
||||
* Contains a number of cached arrays to avoid allocations since calling Enum#values() requires the backing array to be
|
||||
* cloned every time.
|
||||
*/
|
||||
public class DirectionUtil {
|
||||
|
||||
public static final ForgeDirection[] ALL_DIRECTIONS = ForgeDirection.values();
|
||||
|
||||
// Provides the same order as enumerating ForgeDirection and checking the axis of each value
|
||||
public static final ForgeDirection[] HORIZONTAL_DIRECTIONS = new ForgeDirection[] { ForgeDirection.NORTH,
|
||||
ForgeDirection.SOUTH, ForgeDirection.WEST, ForgeDirection.EAST };
|
||||
|
||||
public static final Vector3i[] STEP = {
|
||||
|
||||
// DOWN
|
||||
new Vector3i(0, -1, 0),
|
||||
// UP
|
||||
new Vector3i(0, 1, 0),
|
||||
// NORTH
|
||||
new Vector3i(0, 0, -1),
|
||||
// SOUTH
|
||||
new Vector3i(0, 0, 1),
|
||||
// WEST
|
||||
new Vector3i(-1, 0, 0),
|
||||
// EAST
|
||||
new Vector3i(1, 0, 0),
|
||||
// UNKNOWN
|
||||
new Vector3i(0, 0, 0) };
|
||||
|
||||
public static ForgeDirection rotateDir(ForgeDirection in, Matrix4f rotMat) {
|
||||
|
||||
final Vector3f v = new Vector3f(in.offsetX, in.offsetY, in.offsetZ);
|
||||
v.mulPosition(rotMat);
|
||||
return ModelQuadFacing.toDirection(ModelQuadFacing.fromVector(v));
|
||||
}
|
||||
|
||||
public static ForgeDirection fromName(String name) {
|
||||
return switch (name) {
|
||||
case "up" -> ForgeDirection.UP;
|
||||
case "down" -> ForgeDirection.DOWN;
|
||||
case "north" -> ForgeDirection.NORTH;
|
||||
case "south" -> ForgeDirection.SOUTH;
|
||||
case "west" -> ForgeDirection.WEST;
|
||||
case "east" -> ForgeDirection.EAST;
|
||||
case "unknown" -> ForgeDirection.UNKNOWN;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.util;
|
||||
|
||||
public class MathUtil {
|
||||
|
||||
/**
|
||||
* @return True if the specified number is greater than zero and is a power of two, otherwise false
|
||||
*/
|
||||
public static boolean isPowerOfTwo(int n) {
|
||||
return ((n & (n - 1)) == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Hash of the position, replaces a similar method from modern. It's almost certainly not exactly
|
||||
* equivalent, but I'll be very concerned if it matters
|
||||
*/
|
||||
public static long hashPos(int x, int y, int z) {
|
||||
return cantor(x, cantor(y, z));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps every positive a and b to a unique int, barring overflow
|
||||
* <a href="https://stackoverflow.com/a/73089718">Source on Stack Overflow</a>
|
||||
*/
|
||||
private static long cantor(long a, long b) {
|
||||
return (a + b + 1) * (a + b) / 2 + b;
|
||||
}
|
||||
|
||||
public static boolean fuzzy_eq(float a, float b) {
|
||||
return Math.abs(a - b) < 1e-5;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.util;
|
||||
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
|
||||
/**
|
||||
* Provides some utilities and constants for interacting with vanilla's model quad vertex format.
|
||||
*
|
||||
* This is the current vertex format used by Minecraft for chunk meshes and model quads. Internally, it uses integer
|
||||
* arrays for store baked quad data, and as such the following table provides both the byte and int indices.
|
||||
*
|
||||
* Byte Index Integer Index Name Format Fields 0 ..11 0..2 Position 3 floats x, y, z 12..15 3 Color 4 unsigned bytes a,
|
||||
* r, g, b 16..23 4..5 Block Texture 2 floats u, v 24..27 6 Light Texture 2 shorts u, v 28..30 7 Normal 3 unsigned bytes
|
||||
* x, y, z 31 Padding 1 byte
|
||||
*/
|
||||
public class ModelQuadUtil {
|
||||
|
||||
// Integer indices for vertex attributes, useful for accessing baked quad data
|
||||
public static final int POSITION_INDEX = 0, COLOR_INDEX = 3, TEXTURE_INDEX = 4, LIGHT_INDEX = 6, NORMAL_INDEX = 7;
|
||||
|
||||
// Size of vertex format in 4-byte integers
|
||||
public static final int VERTEX_SIZE = 8;
|
||||
public static final int VERTEX_SIZE_BYTES = VERTEX_SIZE * 4;
|
||||
|
||||
// Cached array of normals for every facing to avoid expensive computation
|
||||
static final int[] NORMALS = new int[ForgeDirection.VALID_DIRECTIONS.length];
|
||||
|
||||
static {
|
||||
for (int i = 0; i < NORMALS.length; i++) {
|
||||
final ForgeDirection dir = ForgeDirection.VALID_DIRECTIONS[i];
|
||||
NORMALS[i] = NormI8.pack(dir.offsetX, dir.offsetY, dir.offsetZ);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normal vector for a model quad with the given {@param facing}.
|
||||
*/
|
||||
public static int getFacingNormal(ForgeDirection facing) {
|
||||
return NORMALS[facing.ordinal()];
|
||||
}
|
||||
|
||||
public static int getFacingNormal(ForgeDirection facing, int bakedNormal) {
|
||||
if (!hasNormal(bakedNormal)) return NORMALS[facing.ordinal()];
|
||||
return bakedNormal;
|
||||
}
|
||||
|
||||
public static boolean hasNormal(int n) {
|
||||
return (n & 0xFFFFFF) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param vertexIndex The index of the vertex to access
|
||||
* @return The starting offset of the vertex's attributes
|
||||
*/
|
||||
public static int vertexOffset(int vertexIndex) {
|
||||
return vertexIndex * VERTEX_SIZE;
|
||||
}
|
||||
|
||||
public static int mergeBakedLight(int packedLight, int calcLight) {
|
||||
// bail early in most cases
|
||||
if (packedLight == 0) return calcLight;
|
||||
|
||||
final int psl = (packedLight >> 16) & 0xFF;
|
||||
final int csl = (calcLight >> 16) & 0xFF;
|
||||
final int pbl = (packedLight) & 0xFF;
|
||||
final int cbl = (calcLight) & 0xFF;
|
||||
final int bl = Math.max(pbl, cbl);
|
||||
final int sl = Math.max(psl, csl);
|
||||
return (sl << 16) | bl;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
package com.gtnewhorizon.gtnhlib.client.renderer.util;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.BlockLiquid;
|
||||
import net.minecraft.block.material.Material;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
import net.minecraftforge.common.util.ForgeDirection;
|
||||
import net.minecraftforge.fluids.Fluid;
|
||||
import net.minecraftforge.fluids.FluidRegistry;
|
||||
import net.minecraftforge.fluids.IFluidBlock;
|
||||
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.BlockPos;
|
||||
|
||||
/**
|
||||
* Contains methods stripped from BlockState or FluidState that didn't actually need to be there. Technically these
|
||||
* could be a mixin to Block or Fluid, but that's annoying while not actually providing any benefit.
|
||||
*/
|
||||
public class WorldUtil {
|
||||
|
||||
public static Vector3d getVelocity(IBlockAccess world, int x, int y, int z, Block thizz) {
|
||||
|
||||
Vector3d velocity = new Vector3d();
|
||||
int decay = getEffectiveFlowDecay(world, x, y, z, thizz);
|
||||
|
||||
for (ForgeDirection dire : DirectionUtil.HORIZONTAL_DIRECTIONS) {
|
||||
|
||||
int adjX = x + dire.offsetX;
|
||||
int adjZ = z + dire.offsetZ;
|
||||
|
||||
int adjDecay = getEffectiveFlowDecay(world, adjX, y, adjZ, thizz);
|
||||
|
||||
if (adjDecay < 0) {
|
||||
|
||||
if (!world.getBlock(adjX, y, adjZ).getMaterial().blocksMovement()) {
|
||||
|
||||
adjDecay = getEffectiveFlowDecay(world, adjX, y - 1, adjZ, thizz);
|
||||
|
||||
if (adjDecay >= 0) {
|
||||
|
||||
adjDecay -= (decay - 8);
|
||||
velocity = velocity.add((adjX - x) * adjDecay, 0, (adjZ - z) * adjDecay);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
adjDecay -= decay;
|
||||
velocity = velocity.add((adjX - x) * adjDecay, 0, (adjZ - z) * adjDecay);
|
||||
}
|
||||
}
|
||||
|
||||
if (world.getBlockMetadata(x, y, z) >= 8) {
|
||||
|
||||
if (thizz.isBlockSolid(world, x, y, z - 1, 2) || thizz.isBlockSolid(world, x, y, z + 1, 3)
|
||||
|| thizz.isBlockSolid(world, x - 1, y, z, 4)
|
||||
|| thizz.isBlockSolid(world, x + 1, y, z, 5)
|
||||
|| thizz.isBlockSolid(world, x, y + 1, z - 1, 2)
|
||||
|| thizz.isBlockSolid(world, x, y + 1, z + 1, 3)
|
||||
|| thizz.isBlockSolid(world, x - 1, y + 1, z, 4)
|
||||
|| thizz.isBlockSolid(world, x + 1, y + 1, z, 5)) {
|
||||
velocity = velocity.normalize().add(0.0D, -6.0D, 0.0D);
|
||||
}
|
||||
}
|
||||
|
||||
if (velocity.x == 0 && velocity.y == 0 && velocity.z == 0) return velocity.zero();
|
||||
return velocity.normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any block in a 3x3x3 cube is not the same fluid and not an opaque full cube. Equivalent to
|
||||
* FluidState::method_15756 in modern.
|
||||
*/
|
||||
public static boolean method_15756(IBlockAccess world, BlockPos pos, Fluid fluid) {
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
Block block = world.getBlock(pos.x, pos.y, pos.z);
|
||||
if (!block.isOpaqueCube() && getFluid(block) != fluid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns fluid height as a percentage of the block; 0 is none and 1 is full.
|
||||
*/
|
||||
public static float getFluidHeight(Fluid fluid, int meta) {
|
||||
return fluid == null ? 0 : 1 - BlockLiquid.getLiquidHeightPercent(meta);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the flow decay but converts values indicating falling liquid (values >=8) to their effective source block
|
||||
* value of zero
|
||||
*/
|
||||
public static int getEffectiveFlowDecay(IBlockAccess world, int x, int y, int z, Block thiz) {
|
||||
|
||||
if (world.getBlock(x, y, z).getMaterial() != thiz.getMaterial()) {
|
||||
|
||||
return -1;
|
||||
} else {
|
||||
|
||||
int decay = world.getBlockMetadata(x, y, z);
|
||||
return decay >= 8 ? 0 : decay;
|
||||
}
|
||||
}
|
||||
|
||||
// I believe forge mappings in modern say BreakableBlock, while yarn says TransparentBlock.
|
||||
// I have a sneaking suspicion isOpaque is neither, but it works for now
|
||||
public static boolean shouldDisplayFluidOverlay(Block block) {
|
||||
return !block.getMaterial().isOpaque() || block.getMaterial() == Material.leaves;
|
||||
}
|
||||
|
||||
public static boolean isFluidBlock(Block block) {
|
||||
return block instanceof IFluidBlock || block instanceof BlockLiquid;
|
||||
}
|
||||
|
||||
public static Fluid getFluid(Block b) {
|
||||
if (b instanceof IFluidBlock fluidBlock) return fluidBlock.getFluid();
|
||||
if (b instanceof BlockLiquid) {
|
||||
if (b.getMaterial() == Material.water) return FluidRegistry.WATER;
|
||||
if (b.getMaterial() == Material.lava) return FluidRegistry.LAVA;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to method_15748 in 1.16.5
|
||||
*/
|
||||
public static boolean isEmptyOrSame(Fluid fluid, Fluid otherFluid) {
|
||||
return otherFluid == null || fluid == otherFluid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to method_15749 in 1.16.5
|
||||
*/
|
||||
public static boolean method_15749(IBlockAccess world, Fluid thiz, BlockPos pos, ForgeDirection dir) {
|
||||
Block b = world.getBlock(pos.x, pos.y, pos.z);
|
||||
Fluid f = getFluid(b);
|
||||
if (f == thiz) {
|
||||
return false;
|
||||
}
|
||||
if (dir == ForgeDirection.UP) {
|
||||
return true;
|
||||
}
|
||||
return b.getMaterial() != Material.ice && b.isSideSolid(world, pos.x, pos.y, pos.z, dir);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.QuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.quad.writers.IWriteQuads;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
@@ -90,7 +90,7 @@ public class VertexFormat {
|
||||
return quadWriter != null;
|
||||
}
|
||||
|
||||
public void writeQuad(QuadView quad, ByteBuffer byteBuffer) {
|
||||
public void writeQuad(ModelQuadView quad, ByteBuffer byteBuffer) {
|
||||
if (quadWriter == null) {
|
||||
throw new IllegalStateException("No quad writer set");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.gtnewhorizon.gtnhlib.concurrent;
|
||||
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
|
||||
import it.unimi.dsi.fastutil.Function;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap;
|
||||
|
||||
/**
|
||||
* Borrowed from ModernFix, because 7.10 can't embed models in blocks easily
|
||||
*/
|
||||
public class ThreadsafeCache<K, V> {
|
||||
|
||||
private final int size;
|
||||
private final Object2ReferenceLinkedOpenHashMap<K, V> cache = new Object2ReferenceLinkedOpenHashMap<>();
|
||||
private final StampedLock lock = new StampedLock();
|
||||
private final Function<K, V> modelRetriever;
|
||||
private final boolean allowNulls;
|
||||
|
||||
public ThreadsafeCache(int size, Function<K, V> modelRetriever, boolean allowNulls) {
|
||||
this.size = size;
|
||||
this.modelRetriever = modelRetriever;
|
||||
this.allowNulls = allowNulls;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
long stamp = lock.writeLock();
|
||||
try {
|
||||
cache.clear();
|
||||
} finally {
|
||||
lock.unlock(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean needToPopulate(K state) {
|
||||
long stamp = lock.readLock();
|
||||
try {
|
||||
return !cache.containsKey(state);
|
||||
} finally {
|
||||
lock.unlock(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
private V getModelFromCache(K state) {
|
||||
long stamp = lock.readLock();
|
||||
try {
|
||||
return cache.get(state);
|
||||
} finally {
|
||||
lock.unlock(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
private V cacheModel(K state) {
|
||||
V model = modelRetriever.apply(state);
|
||||
|
||||
// Lock and modify our local, faster cache
|
||||
long stamp = lock.writeLock();
|
||||
|
||||
try {
|
||||
cache.putAndMoveToFirst(state, model);
|
||||
// TODO: choose less arbitrary number
|
||||
if (cache.size() >= size) {
|
||||
cache.removeLast();
|
||||
}
|
||||
} finally {
|
||||
lock.unlock(stamp);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
public V get(K key) {
|
||||
V model = getModelFromCache(key);
|
||||
|
||||
if (model == null && (!allowNulls || needToPopulate(key))) {
|
||||
model = cacheModel(key);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,12 @@ import org.spongepowered.asm.service.mojang.MixinServiceLaunchWrapper;
|
||||
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.gtnewhorizon.gtnhlib.GTNHLib;
|
||||
import com.gtnewhorizon.gtnhlib.GTNHLibConfig;
|
||||
import com.gtnewhorizon.gtnhlib.Tags;
|
||||
import com.gtnewhorizon.gtnhlib.client.tooltip.LoreHolderDiscoverer;
|
||||
import com.gtnewhorizon.gtnhlib.config.ConfigException;
|
||||
import com.gtnewhorizon.gtnhlib.config.ConfigurationManager;
|
||||
import com.gtnewhorizon.gtnhlib.eventbus.EventBusUtil;
|
||||
import com.gtnewhorizon.gtnhlib.mixins.Mixins;
|
||||
import com.gtnewhorizon.gtnhmixins.IEarlyMixinLoader;
|
||||
@@ -35,6 +39,14 @@ public class GTNHLibCore extends DummyModContainer implements IFMLLoadingPlugin,
|
||||
public static final String[] DEFAULT_TRANSFORMERS = new String[] {
|
||||
"com.gtnewhorizon.gtnhlib.core.transformer.EventBusSubTransformer" };
|
||||
|
||||
static {
|
||||
try {
|
||||
ConfigurationManager.registerConfig(GTNHLibConfig.class);
|
||||
} catch (ConfigException e) {
|
||||
GTNHLib.LOG.error("Failed to register GTNHLib config!", e);
|
||||
}
|
||||
}
|
||||
|
||||
public GTNHLibCore() {
|
||||
super(new ModMetadata());
|
||||
ModMetadata md = getMetadata();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.gtnewhorizon.gtnhlib.mixins;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.GTNHLibConfig.autoTextureLoading;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.util.EarlyConfig;
|
||||
import com.gtnewhorizon.gtnhmixins.builders.IMixins;
|
||||
import com.gtnewhorizon.gtnhmixins.builders.MixinBuilder;
|
||||
@@ -23,7 +25,12 @@ public enum Mixins implements IMixins {
|
||||
.addClientMixins("debug.MixinDynamicTexture", "debug.MixinTextureAtlasSprite").setPhase(Phase.EARLY)
|
||||
.setApplyIf(() -> Boolean.parseBoolean(System.getProperty("gtnhlib.debugtextures", "false")))),
|
||||
FONT_RENDERER(new MixinBuilder("Font rendering replacements").addClientMixins("MixinFontRenderer")
|
||||
.setPhase(Phase.EARLY).setApplyIf(() -> EarlyConfig.enableFontRendererMixin));
|
||||
.setPhase(Phase.EARLY).setApplyIf(() -> EarlyConfig.enableFontRendererMixin)),
|
||||
MODEL_TEXTURE_LOADING(new MixinBuilder("Automatically load model textures").addClientMixins(
|
||||
"models.FRMAccessor",
|
||||
"models.MixinFileResourcePack",
|
||||
"models.MixinFolderResourcePack",
|
||||
"models.SRRMAccessor").setPhase(Phase.EARLY).setApplyIf(() -> autoTextureLoading));
|
||||
|
||||
private final MixinBuilder builder;
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.gtnewhorizon.gtnhlib.mixins.early.models;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.client.resources.FallbackResourceManager;
|
||||
import net.minecraft.client.resources.IResourcePack;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ducks.BackingResourceManager;
|
||||
|
||||
@SuppressWarnings("UnusedMixin")
|
||||
@Mixin(FallbackResourceManager.class)
|
||||
public interface FRMAccessor extends BackingResourceManager {
|
||||
|
||||
@Override
|
||||
@Accessor("resourcePacks")
|
||||
List<IResourcePack> nhlib$getResourcePacks();
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.gtnewhorizon.gtnhlib.mixins.early.models;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry.MODEL_LOGGER;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import net.minecraft.client.resources.AbstractResourcePack;
|
||||
import net.minecraft.client.resources.FileResourcePack;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelResourcePack;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.JSONModel;
|
||||
|
||||
@SuppressWarnings("UnusedMixin")
|
||||
@Mixin(FileResourcePack.class)
|
||||
public abstract class MixinFileResourcePack extends AbstractResourcePack implements ModelResourcePack {
|
||||
|
||||
@Shadow
|
||||
protected abstract ZipFile getResourcePackZipFile() throws IOException;
|
||||
|
||||
public MixinFileResourcePack(File file) {
|
||||
super(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> nhlib$getReferencedTextures(Function<Reader, JSONModel> jsonParser) {
|
||||
try {
|
||||
var zip = getResourcePackZipFile();
|
||||
final var jsons = zip.stream().filter(ze -> {
|
||||
final var name = ze.getName();
|
||||
final var parts = name.split("/");
|
||||
|
||||
// Make sure it's long enough (assets/<domain>/<subdomain>/something.json), make sure the subdomain is
|
||||
// "models", make sure it's a file, and make sure it's a JSON
|
||||
if (parts.length < 4) return false;
|
||||
if (!parts[2].equals("models")) return false;
|
||||
if (ze.isDirectory()) return false;
|
||||
return name.endsWith(".json");
|
||||
}).map(ze -> {
|
||||
|
||||
try {
|
||||
return zip.getInputStream(ze);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
return nhlib$getReferencedTextures(jsons, jsonParser);
|
||||
} catch (Exception e) {
|
||||
|
||||
MODEL_LOGGER.warn("Failed to walk resource pack {}", this);
|
||||
MODEL_LOGGER.warn(e);
|
||||
}
|
||||
|
||||
return ModelResourcePack.super.nhlib$getReferencedTextures(jsonParser);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.gtnewhorizon.gtnhlib.mixins.early.models;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry.MODEL_LOGGER;
|
||||
import static java.nio.file.Files.walk;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.Reader;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import net.minecraft.client.resources.AbstractResourcePack;
|
||||
import net.minecraft.client.resources.FolderResourcePack;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelResourcePack;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.unbaked.JSONModel;
|
||||
|
||||
@SuppressWarnings("UnusedMixin")
|
||||
@Mixin(FolderResourcePack.class)
|
||||
public abstract class MixinFolderResourcePack extends AbstractResourcePack implements ModelResourcePack {
|
||||
|
||||
public MixinFolderResourcePack(File folder) {
|
||||
super(folder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> nhlib$getReferencedTextures(Function<Reader, JSONModel> jsonParser) {
|
||||
try (var files = walk(resourcePackFile.toPath(), FileVisitOption.FOLLOW_LINKS)) {
|
||||
|
||||
final var jsons = files.filter(p -> {
|
||||
// Get the relative path
|
||||
final var path = resourcePackFile.toPath().relativize(p);
|
||||
|
||||
// Make sure it's long enough (<domain>/<subdomain>/something.json), make sure the subdomain is
|
||||
// "models", make sure it's a file, and make sure it's a JSON
|
||||
if (path.getNameCount() < 3) return false;
|
||||
if (!path.getName(1).toString().equals("models")) return false;
|
||||
if (!path.toFile().isFile()) return false;
|
||||
return path.endsWith(".json");
|
||||
}).map(p -> {
|
||||
try {
|
||||
return new FileInputStream(p.toFile());
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
return nhlib$getReferencedTextures(jsons, jsonParser);
|
||||
} catch (Exception e) {
|
||||
MODEL_LOGGER.warn("Failed to walk resource pack {}", this);
|
||||
MODEL_LOGGER.warn(e);
|
||||
}
|
||||
|
||||
return ModelResourcePack.super.nhlib$getReferencedTextures(jsonParser);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.gtnewhorizon.gtnhlib.mixins.early.models;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import net.minecraft.client.resources.FallbackResourceManager;
|
||||
import net.minecraft.client.resources.SimpleReloadableResourceManager;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ducks.GlobalResourceManager;
|
||||
|
||||
@SuppressWarnings("UnusedMixin")
|
||||
@Mixin(SimpleReloadableResourceManager.class)
|
||||
public interface SRRMAccessor extends GlobalResourceManager {
|
||||
|
||||
@Override
|
||||
@Accessor("domainResourceManagers")
|
||||
Map<String, FallbackResourceManager> nhlib$getDomainResourceManagers();
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.gtnewhorizon.gtnhlib.util.map;
|
||||
|
||||
/*
|
||||
* Copyright (c) 2022, GTNH Team, glee8e, Code Chicken, This file is originally part of NotEnoughtItem by Code Chicken.
|
||||
* It is adapted to implement the standard Map interface by glee8e This program is free software: you can redistribute
|
||||
* It is adapted to implement the standard Maps interface by glee8e This program is free software: you can redistribute
|
||||
* it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software
|
||||
* Foundation, either version 2.1 of the License, or (at your option) any later version. This program is distributed in
|
||||
* the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
@@ -39,7 +39,7 @@ import com.google.common.collect.Iterators;
|
||||
* <p>
|
||||
* This map does NOT support null values or null keys! null will be silently ignored!
|
||||
* <p>
|
||||
* Originally created by CodeChicken for NotEnoughItems. Adapted to {@code Map<ItemStack, T>} interface by glee8e
|
||||
* Originally created by CodeChicken for NotEnoughItems. Adapted to {@code Maps<ItemStack, T>} interface by glee8e
|
||||
* <p>
|
||||
* Edited later to add the option to ignore NBT tags to match vanilla furnace recipe map behavior.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"variants": {
|
||||
"meta=0": { "model": "gtnhlib:blocks/old_stairs", "y": 270 },
|
||||
"meta=1": { "model": "gtnhlib:blocks/old_stairs", "y": 90 },
|
||||
"meta=2": { "model": "gtnhlib:blocks/old_stairs", "y": 0 },
|
||||
"meta=3": { "model": "gtnhlib:blocks/old_stairs", "y": 180 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "block/cube_all",
|
||||
"textures": {
|
||||
"all": "minecraft:stone"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"parent": "minecraft:block/stairs",
|
||||
"textures": {
|
||||
"bottom": "minecraft:planks_oak",
|
||||
"side": "minecraft:planks_oak",
|
||||
"top": "minecraft:planks_oak"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"gui_light": "side",
|
||||
"display": {
|
||||
"gui": {
|
||||
"rotation": [ 30, 225, 0 ],
|
||||
"translation": [ 0, 0, 0],
|
||||
"scale":[ 0.625, 0.625, 0.625 ]
|
||||
},
|
||||
"ground": {
|
||||
"rotation": [ 0, 0, 0 ],
|
||||
"translation": [ 0, 3, 0],
|
||||
"scale":[ 0.25, 0.25, 0.25 ]
|
||||
},
|
||||
"fixed": {
|
||||
"rotation": [ 0, 0, 0 ],
|
||||
"translation": [ 0, 0, 0],
|
||||
"scale":[ 0.5, 0.5, 0.5 ]
|
||||
},
|
||||
"thirdperson_righthand": {
|
||||
"rotation": [ 75, 45, 0 ],
|
||||
"translation": [ 0, 2.5, 0],
|
||||
"scale": [ 0.375, 0.375, 0.375 ]
|
||||
},
|
||||
"firstperson_righthand": {
|
||||
"rotation": [ 0, 45, 0 ],
|
||||
"translation": [ 0, 0, 0 ],
|
||||
"scale": [ 0.40, 0.40, 0.40 ]
|
||||
},
|
||||
"firstperson_lefthand": {
|
||||
"rotation": [ 0, 225, 0 ],
|
||||
"translation": [ 0, 0, 0 ],
|
||||
"scale": [ 0.40, 0.40, 0.40 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"parent": "block/block",
|
||||
"elements": [
|
||||
{ "from": [ 0, 0, 0 ],
|
||||
"to": [ 16, 16, 16 ],
|
||||
"faces": {
|
||||
"down": { "texture": "#down", "cullface": "down" },
|
||||
"up": { "texture": "#up", "cullface": "up" },
|
||||
"north": { "texture": "#north", "cullface": "north" },
|
||||
"south": { "texture": "#south", "cullface": "south" },
|
||||
"west": { "texture": "#west", "cullface": "west" },
|
||||
"east": { "texture": "#east", "cullface": "east" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user