Json model retexturing (kinda works)

This commit is contained in:
quat1024 2023-07-04 01:00:41 -04:00
parent e034fae532
commit 614be779b8
24 changed files with 407 additions and 39 deletions

4
.gitignore vendored
View File

@ -27,3 +27,7 @@ Thumbs.db
.DS_Store
local.properties
.directory
# krita Moments
*.png~
*-autosave.kra

View File

@ -30,7 +30,7 @@ You may create your block any way you like, just make sure it has a block entity
## `Mesh`-based models
We will construct a `RetexturedMeshTemplateUnbakedModel`. You need two things - the ID of a parent model, and a `Supplier<Mesh>` to retexture.
We will construct a `RetexturedMeshUnbakedModel`. You need two things - the ID of a parent model, and a `Supplier<Mesh>` to retexture.
Fill in the parent model field with the ID of any model. Ideally, this model should have a parent of `block/block`, or at least define *some* non-default rotations (smokey the bear voice *Only You Can Prevent Weirdly Rotated First-Person Models*), set `"gui_light": "front"` (lest the item model look weird), and define a particle texture.
@ -38,7 +38,7 @@ When building the `Mesh`, if you want a face to be dynamically retextured, `.tag
(TODO: implement a system for baking unchanging `Sprite`s onto the mesh, potentially by "registering" more tags; the problem is you don't have access to sprite uvs at mesh building time. Or just provide a way to get sprite UVs at mesh building time...?)
That's all you need in order to construct a `RetexturedMeshTemplateUnbakedModel`, so to finish things off:
That's all you need in order to construct a `RetexturedMeshUnbakedModel`, so to finish things off:
* Come up with an ID for it
* Register it using `TemplatesClient.provider.addTemplateModel` (a thin wrapper around Fabric's `ModelResourceProvider`)

View File

@ -1,6 +1,7 @@
package io.github.cottonmc.templates;
import io.github.cottonmc.templates.block.SlopeBlock;
import io.github.cottonmc.templates.block.TemplateSlabBlock;
import io.github.cottonmc.templates.block.entity.TemplateEntity;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.object.builder.v1.block.entity.FabricBlockEntityTypeBuilder;
@ -26,20 +27,31 @@ public class Templates implements ModInitializer {
FabricBlockEntityTypeBuilder.create(Templates::makeSlopeEntity, SLOPE).build(null)
);
public static final Block SLAB = Registry.register(Registries.BLOCK, id("slab"), new TemplateSlabBlock());
public static final BlockEntityType<TemplateEntity> SLAB_ENTITY = Registry.register(
Registries.BLOCK_ENTITY_TYPE, id("slab"),
FabricBlockEntityTypeBuilder.create(Templates::makeSlabEntity, SLAB).build(null)
);
//Overridden in TemplatesClient
public static BiConsumer<World, BlockPos> chunkRerenderProxy = (world, pos) -> {};
@Override
public void onInitialize() {
Registry.register(Registries.ITEM, id("slope"), (Item) new BlockItem(SLOPE, new Item.Settings()));
Registry.register(Registries.ITEM, id("slope"), new BlockItem(SLOPE, new Item.Settings()));
Registry.register(Registries.ITEM, id("slab"), new BlockItem(SLAB, new Item.Settings()));
}
public static Identifier id(String path) {
return new Identifier(MODID, path);
}
//simply for breaking the circular reference in the SLOPE_ENTITY constructor call
//simply for breaking circular references in the registration calls
private static TemplateEntity makeSlopeEntity(BlockPos pos, BlockState state) {
return new TemplateEntity(SLOPE_ENTITY, pos, state);
}
private static TemplateEntity makeSlabEntity(BlockPos pos, BlockState state) {
return new TemplateEntity(SLAB_ENTITY, pos, state);
}
}

View File

@ -1,6 +1,7 @@
package io.github.cottonmc.templates;
import io.github.cottonmc.templates.model.RetexturedMeshTemplateUnbakedModel;
import io.github.cottonmc.templates.model.RetexturedJsonModelUnbakedModel;
import io.github.cottonmc.templates.model.RetexturedMeshUnbakedModel;
import io.github.cottonmc.templates.model.SlopeBaseMesh;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap;
@ -53,9 +54,14 @@ public class TemplatesClient implements ClientModInitializer {
ModelLoadingRegistry.INSTANCE.registerResourceProvider(rm -> provider); //block models
ModelLoadingRegistry.INSTANCE.registerVariantProvider(rm -> provider); //item models
BlockRenderLayerMap.INSTANCE.putBlock(Templates.SLOPE, RenderLayer.getCutout());
BlockRenderLayerMap.INSTANCE.putBlocks(RenderLayer.getCutout(), Templates.SLOPE, Templates.SLAB);
provider.addTemplateModel(Templates.id("slope_special"), () -> new RetexturedMeshTemplateUnbakedModel(Templates.id("block/slope_base"), SlopeBaseMesh::make));
provider.addTemplateModel(Templates.id("slope_special"), () -> new RetexturedMeshUnbakedModel(Templates.id("block/slope_base"), SlopeBaseMesh::make));
provider.assignItemModel(Templates.id("slope_special"), Templates.SLOPE);
provider.addTemplateModel(Templates.id("cube_special"), () -> new RetexturedJsonModelUnbakedModel(Templates.id("block/cube")));
provider.addTemplateModel(Templates.id("slab_bottom_special"), () -> new RetexturedJsonModelUnbakedModel(Templates.id("block/slab_bottom")));
provider.addTemplateModel(Templates.id("slab_top_special"), () -> new RetexturedJsonModelUnbakedModel(Templates.id("block/slab_top")));
provider.assignItemModel(Templates.id("slab_bottom_special"), Templates.SLAB);
}
}

View File

@ -0,0 +1,29 @@
package io.github.cottonmc.templates.block;
import io.github.cottonmc.templates.Templates;
import net.minecraft.block.BlockEntityProvider;
import net.minecraft.block.BlockState;
import net.minecraft.block.SlabBlock;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.sound.BlockSoundGroup;
import net.minecraft.util.math.BlockPos;
import org.jetbrains.annotations.Nullable;
public class TemplateSlabBlock extends SlabBlock implements BlockEntityProvider {
public TemplateSlabBlock(Settings settings) {
super(settings);
}
public TemplateSlabBlock() {
//super(TemplateBlock.configureSettings(Settings.create()) //TODO
super(Settings.create().nonOpaque()
.sounds(BlockSoundGroup.WOOD)
.hardness(0.2f)); //TODO: Material.WOOD
}
@Nullable
@Override
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return Templates.SLAB_ENTITY.instantiate(pos, state);
}
}

View File

@ -6,20 +6,24 @@ import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh;
import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder;
import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter;
import net.minecraft.util.math.AffineTransformation;
import net.minecraft.util.math.Direction;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import java.util.EnumMap;
import java.util.Map;
/**
* Transforms the position of each vertex in a `Mesh`.
* The transformation's origin is bumped to (0.5, 0.5, 0.5) just because it's more convenient for me lol.
*/
public class MatrixMeshTransformer {
public static Mesh transformAroundCenter(AffineTransformation aff, Mesh oldMesh) {
return transformAroundCenter(aff.getMatrix(), oldMesh);
public class MatrixTransformer {
public static Mesh meshAroundCenter(AffineTransformation aff, Mesh oldMesh) {
return meshAroundCenter(aff.getMatrix(), oldMesh);
}
public static Mesh transformAroundCenter(Matrix4f mat, Mesh oldMesh) {
public static Mesh meshAroundCenter(Matrix4f mat, Mesh oldMesh) {
Renderer r = TemplatesClient.getFabricRenderer();
MeshBuilder newMesh = r.meshBuilder();
QuadEmitter emitter = newMesh.getEmitter();
@ -54,4 +58,22 @@ public class MatrixMeshTransformer {
return newMesh.build();
}
//Hard to explain what this is for...
//Basically, the previous incarnation of this mod assembled the north/south/east/west faces all individually.
//This means it was easy to get the orientation of the block correct - to popular the north face of the slope, look at
//the north texture of the theme block. In this version, there is only *one* slope model that is dynamically rotated
//to form the other possible orientations. If I populate the north face of the model using the north face of the theme,
//that model will then be rotated so it's no longer facing the right way.
//
//This seems to work, but I'm kinda surprised I don't need to invert the transformation here, which is a clue that
//I don't really understand all the math, loool
public static Map<Direction, Direction> facePermutation(AffineTransformation aff) {
Map<Direction, Direction> facePermutation = new EnumMap<>(Direction.class);
for(Direction input : Direction.values()) {
Direction output = Direction.transform(aff.getMatrix(), input);
facePermutation.put(input, output);
}
return facePermutation;
}
}

View File

@ -0,0 +1,135 @@
package io.github.cottonmc.templates.model;
import io.github.cottonmc.templates.Templates;
import io.github.cottonmc.templates.TemplatesClient;
import net.fabricmc.fabric.api.renderer.v1.Renderer;
import net.fabricmc.fabric.api.renderer.v1.material.RenderMaterial;
import net.fabricmc.fabric.api.renderer.v1.mesh.Mesh;
import net.fabricmc.fabric.api.renderer.v1.mesh.MeshBuilder;
import net.fabricmc.fabric.api.renderer.v1.mesh.MutableQuadView;
import net.fabricmc.fabric.api.renderer.v1.mesh.QuadEmitter;
import net.fabricmc.fabric.api.renderer.v1.mesh.QuadView;
import net.fabricmc.fabric.api.renderer.v1.model.ForwardingBakedModel;
import net.fabricmc.fabric.api.renderer.v1.render.RenderContext;
import net.fabricmc.fabric.api.rendering.data.v1.RenderAttachedBlockView;
import net.minecraft.block.BlockState;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.BakedQuad;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.SpriteIdentifier;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.PlayerScreenHandler;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.random.Random;
import net.minecraft.world.BlockRenderView;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
public class RetexturedJsonModelBakedModel extends ForwardingBakedModel {
public RetexturedJsonModelBakedModel(BakedModel baseModel, TemplateAppearanceManager tam, Function<SpriteIdentifier, Sprite> spriteLookup) {
this.wrapped = baseModel;
this.tam = tam;
for(int i = 0; i < DIRECTIONS.length; i++) {
SpriteIdentifier id = new SpriteIdentifier(PlayerScreenHandler.BLOCK_ATLAS_TEXTURE, Templates.id("templates_special/" + DIRECTIONS[i].getName()));
this.specialSprites[i] = Objects.requireNonNull(spriteLookup.apply(id), () -> "Couldn't find sprite " + id + " !");
}
}
private record CacheKey(BlockState state, TemplateAppearance appearance) {}
private final TemplateAppearanceManager tam;
private final ConcurrentHashMap<CacheKey, Mesh> meshCache = new ConcurrentHashMap<>();
private final Sprite[] specialSprites = new Sprite[DIRECTIONS.length];
@Override
public boolean isVanillaAdapter() {
return false;
}
@Override
public void emitBlockQuads(BlockRenderView blockView, BlockState state, BlockPos pos, Supplier<Random> randomSupplier, RenderContext context) {
BlockState template = (((RenderAttachedBlockView) blockView).getBlockEntityRenderAttachment(pos) instanceof BlockState s) ? s : null;
TemplateAppearance ta = template == null || template.isAir() ? tam.getDefaultAppearance() : tam.getAppearance(template);
CacheKey key = new CacheKey(state, ta);
context.meshConsumer().accept(meshCache.computeIfAbsent(key, this::makeMesh));
}
@Override
public void emitItemQuads(ItemStack stack, Supplier<Random> randomSupplier, RenderContext context) {
super.emitItemQuads(stack, randomSupplier, context);
}
protected Mesh makeMesh(CacheKey key) {
Renderer r = TemplatesClient.getFabricRenderer();
MeshBuilder builder = r.meshBuilder();
QuadEmitter emitter = builder.getEmitter();
RenderMaterial mat = key.appearance().getRenderMaterial();
Random rand = Random.create(42);
for(Direction cullFace : DIRECTIONS_AND_NULL) {
for(BakedQuad quad : wrapped.getQuads(key.state, cullFace, rand)) {
emitter.fromVanilla(quad, mat, cullFace);
QuadUvBounds bounds = QuadUvBounds.read(emitter);
for(int i = 0; i < specialSprites.length; i++) {
if(bounds.displaysSprite(specialSprites[i])) {
bounds.remap(emitter, specialSprites[i], key.appearance().getSprite(DIRECTIONS[i]));
break;
}
}
emitter.emit();
}
}
return builder.build();
}
record QuadUvBounds(float minU, float maxU, float minV, float maxV) {
static QuadUvBounds read(QuadView quad) {
float u0 = quad.u(0); float u1 = quad.u(1); float u2 = quad.u(2); float u3 = quad.u(3);
float v0 = quad.v(0); float v1 = quad.v(1); float v2 = quad.v(2); float v3 = quad.v(3);
return new QuadUvBounds(
Math.min(Math.min(u0, u1), Math.min(u2, u3)),
Math.max(Math.max(u0, u1), Math.max(u2, u3)),
Math.min(Math.min(v0, v1), Math.min(v2, v3)),
Math.max(Math.max(v0, v1), Math.max(v2, v3))
);
}
boolean displaysSprite(Sprite sprite) {
return sprite.getMinU() <= minU && sprite.getMaxU() >= maxU && sprite.getMinV() <= minV && sprite.getMaxV() >= maxV;
}
void remap(MutableQuadView quad, Sprite specialSprite, Sprite newSprite) {
float remappedMinU = rangeRemap(minU, specialSprite.getMinU(), specialSprite.getMaxU(), newSprite.getMinU(), newSprite.getMaxU());
float remappedMaxU = rangeRemap(maxU, specialSprite.getMinU(), specialSprite.getMaxU(), newSprite.getMinU(), newSprite.getMaxU());
float remappedMinV = rangeRemap(minV, specialSprite.getMinV(), specialSprite.getMaxV(), newSprite.getMinV(), newSprite.getMaxV());
float remappedMaxV = rangeRemap(maxV, specialSprite.getMinV(), specialSprite.getMaxV(), newSprite.getMinV(), newSprite.getMaxV());
quad.uv(0, MathHelper.approximatelyEquals(quad.u(0), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(0), minV) ? remappedMinV : remappedMaxV);
quad.uv(1, MathHelper.approximatelyEquals(quad.u(1), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(1), minV) ? remappedMinV : remappedMaxV);
quad.uv(2, MathHelper.approximatelyEquals(quad.u(2), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(2), minV) ? remappedMinV : remappedMaxV);
quad.uv(3, MathHelper.approximatelyEquals(quad.u(3), minU) ? remappedMinU : remappedMaxU, MathHelper.approximatelyEquals(quad.v(3), minV) ? remappedMinV : remappedMaxV);
}
static float rangeRemap(float value, float low1, float high1, float low2, float high2) {
float value2 = MathHelper.clamp(value, low1, high1);
return low2 + (value2 - low1) * (high2 - low2) / (high1 - low1);
}
}
private static final Direction[] DIRECTIONS = Direction.values();
private static final Direction[] DIRECTIONS_AND_NULL = new Direction[DIRECTIONS.length + 1];
static {
System.arraycopy(DIRECTIONS, 0, DIRECTIONS_AND_NULL, 0, DIRECTIONS.length);
}
}

View File

@ -0,0 +1,43 @@
package io.github.cottonmc.templates.model;
import io.github.cottonmc.templates.TemplatesClient;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.Baker;
import net.minecraft.client.render.model.ModelBakeSettings;
import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.SpriteIdentifier;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Function;
public class RetexturedJsonModelUnbakedModel implements UnbakedModel {
public RetexturedJsonModelUnbakedModel(Identifier parent) {
this.parent = parent;
}
protected final Identifier parent;
@Override
public Collection<Identifier> getModelDependencies() {
return Collections.singletonList(parent);
}
@Override
public void setParents(Function<Identifier, UnbakedModel> function) {
function.apply(parent).setParents(function);
}
@Nullable
@Override
public BakedModel bake(Baker baker, Function<SpriteIdentifier, Sprite> spriteLookup, ModelBakeSettings modelBakeSettings, Identifier identifier) {
return new RetexturedJsonModelBakedModel(
baker.bake(parent, modelBakeSettings),
TemplatesClient.provider.getOrCreateTemplateApperanceManager(spriteLookup),
spriteLookup
);
}
}

View File

@ -22,36 +22,20 @@ import net.minecraft.util.math.random.Random;
import net.minecraft.world.BlockRenderView;
import org.jetbrains.annotations.NotNull;
import java.util.EnumMap;
import java.util.Map;
import java.util.function.Supplier;
public final class TemplateBakedModel extends ForwardingBakedModel {
public TemplateBakedModel(BakedModel baseModel, TemplateAppearanceManager tam, AffineTransformation aff, Mesh baseMesh) {
public final class RetexturedMeshBakedModel extends ForwardingBakedModel {
public RetexturedMeshBakedModel(BakedModel baseModel, TemplateAppearanceManager tam, AffineTransformation aff, Mesh baseMesh) {
this.wrapped = baseModel;
this.tam = tam;
this.baseMesh = MatrixMeshTransformer.transformAroundCenter(aff, baseMesh);
//Hard to explain what this is for...
//Basically, the previous incarnation of this mod assembled the north/south/east/west faces all individually.
//This means it was easy to get the orientation of the block correct - to popular the north face of the slope, look at
//the north texture of the theme block. In this version, there is only *one* slope model that is dynamically rotated
//to form the other possible orientations. If I populate the north face of the model using the north face of the theme,
//that model will then be rotated so it's no longer facing the right way.
//
//This seems to work, but I'm kinda surprised I don't need to invert the transformation here, which is a clue that
//I don't really understand all the math, loool
for(Direction input : Direction.values()) {
Direction output = Direction.transform(aff.getMatrix(), input);
facePermutation.put(input, output);
}
this.baseMesh = MatrixTransformer.meshAroundCenter(aff, baseMesh);
this.facePermutation = MatrixTransformer.facePermutation(aff);
}
private final TemplateAppearanceManager tam;
private final Mesh baseMesh;
private final Map<Direction, Direction> facePermutation = new EnumMap<>(Direction.class);
private final Map<Direction, Direction> facePermutation;
@Override
public boolean isVanillaAdapter() {

View File

@ -16,8 +16,8 @@ import java.util.function.Function;
import java.util.function.Supplier;
@SuppressWarnings("ClassCanBeRecord")
public class RetexturedMeshTemplateUnbakedModel implements UnbakedModel {
public RetexturedMeshTemplateUnbakedModel(Identifier parent, Supplier<Mesh> baseMeshFactory) {
public class RetexturedMeshUnbakedModel implements UnbakedModel {
public RetexturedMeshUnbakedModel(Identifier parent, Supplier<Mesh> baseMeshFactory) {
this.parent = parent;
this.baseMeshFactory = baseMeshFactory;
}
@ -37,7 +37,7 @@ public class RetexturedMeshTemplateUnbakedModel implements UnbakedModel {
@Override
public BakedModel bake(Baker baker, Function<SpriteIdentifier, Sprite> spriteLookup, ModelBakeSettings modelBakeSettings, Identifier identifier) {
return new TemplateBakedModel(
return new RetexturedMeshBakedModel(
baker.bake(parent, modelBakeSettings),
TemplatesClient.provider.getOrCreateTemplateApperanceManager(spriteLookup),
modelBakeSettings.getRotation(),

View File

@ -9,7 +9,7 @@ import net.minecraft.util.math.Direction;
public class SlopeBaseMesh {
/**
* @see TemplateBakedModel.RetexturingTransformer for why these values were chosen
* @see RetexturedMeshBakedModel.RetexturingTransformer for why these values were chosen
*/
public static final int TAG_SLOPE = Direction.UP.ordinal() + 1;
public static final int TAG_LEFT = Direction.EAST.ordinal() + 1;

View File

@ -0,0 +1,9 @@
{
"sources": [
{
"type": "directory",
"source": "templates_special",
"prefix": "templates_special/"
}
]
}

View File

@ -0,0 +1,13 @@
{
"variants": {
"type=bottom": {
"model": "templates:slab_bottom_special"
},
"type=double": {
"model": "templates:cube_special"
},
"type=top": {
"model": "templates:slab_top_special"
}
}
}

View File

@ -1,3 +1,4 @@
{
"block.templates.slope": "Slope Template"
"block.templates.slope": "Slope Template",
"block.templates.slab": "Slab Template"
}

View File

@ -0,0 +1,12 @@
{
"parent": "minecraft:block/cube",
"textures": {
"down": "templates:templates_special/down",
"up": "templates:templates_special/up",
"north": "templates:templates_special/north",
"south": "templates:templates_special/south",
"west": "templates:templates_special/west",
"east": "templates:templates_special/east",
"particle": "minecraft:block/scaffolding_top"
}
}

View File

@ -0,0 +1,49 @@
{
"parent": "minecraft:block/block",
"textures": {
"down": "templates:templates_special/down",
"up": "templates:templates_special/up",
"north": "templates:templates_special/north",
"south": "templates:templates_special/south",
"west": "templates:templates_special/west",
"east": "templates:templates_special/east",
"particle": "minecraft:block/scaffolding_top"
},
"elements": [
{
"from": [0, 0, 0],
"to": [16, 8, 16],
"faces": {
"down": {
"uv": [0, 0, 16, 16],
"texture": "#down",
"cullface": "down"
},
"up": {
"uv": [0, 0, 16, 16],
"texture": "#up"
},
"north": {
"uv": [0, 8, 16, 16],
"texture": "#north",
"cullface": "north"
},
"south": {
"uv": [0, 8, 16, 16],
"texture": "#south",
"cullface": "south"
},
"west": {
"uv": [0, 8, 16, 16],
"texture": "#west",
"cullface": "west"
},
"east": {
"uv": [0, 8, 16, 16],
"texture": "#east",
"cullface": "east"
}
}
}
]
}

View File

@ -0,0 +1,49 @@
{
"parent": "minecraft:block/block",
"textures": {
"down": "templates:templates_special/down",
"up": "templates:templates_special/up",
"north": "templates:templates_special/north",
"south": "templates:templates_special/south",
"west": "templates:templates_special/west",
"east": "templates:templates_special/east",
"particle": "minecraft:block/scaffolding_top"
},
"elements": [
{
"from": [0, 8, 0],
"to": [16, 16, 16],
"faces": {
"down": {
"uv": [0, 0, 16, 16],
"texture": "#down"
},
"up": {
"uv": [0, 0, 16, 16],
"texture": "#up",
"cullface": "up"
},
"north": {
"uv": [0, 0, 16, 8],
"texture": "#north",
"cullface": "north"
},
"south": {
"uv": [0, 0, 16, 8],
"texture": "#south",
"cullface": "south"
},
"west": {
"uv": [0, 0, 16, 8],
"texture": "#west",
"cullface": "west"
},
"east": {
"uv": [0, 0, 16, 8],
"texture": "#east",
"cullface": "east"
}
}
}
]
}

View File

@ -1,8 +1,8 @@
{
"parent": "block/block",
"parent": "minecraft:block/block",
"gui_light": "front",
"textures": {
"particle": "block/scaffolding_top"
"particle": "minecraft:block/scaffolding_top"
},
"display": {
"firstperson_righthand": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B