Updates to JSON model API (#100)

This commit is contained in:
ah-OOG-ah authored and GitHub committed 2025-11-06 12:25:11 -05:00
1 parent 6d428aea87
commit 6529f347ec
102 files changed
+3991 -3575

No files matched your search

+2
View File
@@ -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")
+23
View File
@@ -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);
}
}
}
@@ -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();
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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();
}
@@ -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);
}
@@ -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;
};
}
}
}
@@ -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;
}
}
@@ -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;
@@ -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);
}
@@ -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;
};
}
}
@@ -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,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);
}
@@ -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));
}
}
}
@@ -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));
@@ -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));
@@ -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));
@@ -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);
}
}
@@ -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