Repository navigation
Expand file tree
/
Copy pathLongboatModels.java
More file actions
142 lines (134 loc) · 7.45 KB
/
Copy pathLongboatModels.java
File metadata and controls
142 lines (134 loc) · 7.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package com.xc.longboatlab.client;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.xc.longboatlab.LongboatLab;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener;
import net.minecraft.client.model.ModelData;
import net.minecraft.client.model.ModelPart;
import net.minecraft.client.model.ModelPartBuilder;
import net.minecraft.client.model.ModelTransform;
import net.minecraft.client.model.TexturedModelData;
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
/** The packaged Blockbench projects are the actual source of the entity mesh. */
public final class LongboatModels implements SimpleSynchronousResourceReloadListener {
public static final LongboatModels INSTANCE = new LongboatModels();
private record HullSize(int segments, int width) {}
private final Map<HullSize, ModelPart> hulls = boundedCache();
private final Map<Integer, ModelPart> waterMasks = boundedCache();
private static <K> Map<K, ModelPart> boundedCache() {
return new java.util.LinkedHashMap<K, ModelPart>(32, 0.75f, true) {
@Override protected boolean removeEldestEntry(Map.Entry<K, ModelPart> entry) { return size() > 32; }
};
}
private List<Cube> hullTemplate = List.of();
private ModelPart oar;
private record Cube(String name, float x, float y, float z, float width, float height, float depth, int u, int v) {}
@Override public Identifier getFabricId() { return Identifier.of(LongboatLab.ID, "boat_models"); }
@Override public void reload(ResourceManager manager) {
hullTemplate = read(manager, "continuous_hull");
oar = bake(read(manager, "oar"), 2, 1, false);
hulls.clear();
waterMasks.clear();
}
public ModelPart hull(int segments) {
return hull(segments, 1);
}
private ModelPart hull(int segments, int width) {
return hulls.computeIfAbsent(new HullSize(segments, width), size -> bake(hullTemplate, size.segments, size.width, true));
}
/** Animate length; the width and its box UVs are baked together at native texel density. */
public ModelPart prepareHull(int segments, double half, float extension, int width) {
ModelPart hull = hull(segments, width);
for (Cube cube : hullTemplate) {
if (cube.name.startsWith("seat_")) continue;
ModelPart part = hull.getChild(cube.name);
part.zScale = 1; part.pivotZ = 0;
if (cube.name.startsWith("stretch_")) {
double originalStart = cube.z - (segments - 2) * 16.0;
double desiredStart = cube.z - (half - 2) * 16;
part.zScale = (float) ((cube.depth + (half - 2) * 32) / (cube.depth + (segments - 2) * 32.0));
part.pivotZ = (float) (desiredStart - originalStart * part.zScale);
} else if (cube.name.startsWith("bow_")) part.pivotZ = (float) ((half - segments) * 16);
else if (cube.name.startsWith("stern_")) part.pivotZ = (float) ((segments - half) * 16);
}
for (int i = 1; i <= Math.min(512, segments - 1); i++) {
ModelPart divider = hull.getChild("divider_" + i);
divider.zScale = extension;
divider.yScale = extension;
divider.visible = extension > 0.001f;
}
return hull;
}
public ModelPart oar() { return oar; }
public ModelPart waterMask(int segments) {
return waterMasks.computeIfAbsent(segments, length -> {
ModelData data = new ModelData();
data.getRoot().addChild("water_mask", ModelPartBuilder.create()
.cuboid(-9, 2, -length * 16f + 2, 18, 6, length * 32f - 4), ModelTransform.NONE);
return TexturedModelData.of(data, 2048, 1024).createModel();
});
}
private static List<Cube> read(ResourceManager manager, String file) {
Identifier id = Identifier.of(LongboatLab.ID, "models/entity/" + file + ".bbmodel");
try (Reader reader = manager.getResourceOrThrow(id).getReader()) {
JsonObject root = JsonParser.parseReader(reader).getAsJsonObject();
List<Cube> cubes = new ArrayList<>();
for (var element : root.getAsJsonArray("elements")) {
JsonObject cube = element.getAsJsonObject();
JsonArray from = cube.getAsJsonArray("from"), to = cube.getAsJsonArray("to");
JsonArray uv = cube.getAsJsonArray("uv_offset");
cubes.add(new Cube(cube.get("name").getAsString(), value(from, 0), value(from, 1), value(from, 2),
value(to, 0) - value(from, 0), value(to, 1) - value(from, 1), value(to, 2) - value(from, 2),
uv.get(0).getAsInt(), uv.get(1).getAsInt()));
}
if (cubes.isEmpty()) throw new IllegalStateException("Empty Blockbench model: " + id);
return List.copyOf(cubes);
} catch (IOException exception) {
throw new IllegalStateException("Cannot read Blockbench model " + id, exception);
}
}
private static float value(JsonArray array, int index) { return array.get(index).getAsFloat(); }
private static ModelPart bake(List<Cube> cubes, int segments, int width, boolean extend) {
ModelData model = new ModelData();
float extension = (segments - 2) * 16f;
for (Cube cube : cubes) {
if (extend && cube.name().startsWith("seat_")) continue;
float x = cube.x(), cubeWidth = cube.width(), z = cube.z(), depth = cube.depth();
if (extend) {
if (cube.name().startsWith("stretch_left_")) x += (width - 1) * 11f;
else if (cube.name().startsWith("stretch_right_")) x -= (width - 1) * 11f;
else { x *= width; cubeWidth *= width; }
}
if (extend && cube.name().startsWith("stretch_")) {
z -= extension;
depth += extension * 2;
} else if (extend && cube.name().startsWith("bow_")) z += extension;
else if (extend && cube.name().startsWith("stern_")) z -= extension;
model.getRoot().addChild(cube.name(), ModelPartBuilder.create().uv(cube.u(), cube.v())
// Box UV extents grow with the geometry, sampling more of the tiled
// wood atlas instead of scaling its pixels. Sidewall thickness stays fixed.
.cuboid(x, cube.y(), z, cubeWidth, cube.height(), depth), ModelTransform.NONE);
}
if (extend) {
Cube crossbar = cubes.stream().filter(c -> c.name().equals("seat_center")).findFirst().orElseThrow();
float crossbarScale = (22f * width - 4) / 18;
int dividers = Math.min(512, segments - 1);
for (int i = 1; i <= dividers; i++) {
long boundary = (long) i * segments / (dividers + 1);
float z = -segments * 16f + boundary * 32f - crossbar.depth() / 2;
model.getRoot().addChild("divider_" + i, ModelPartBuilder.create().uv(crossbar.u(), crossbar.v())
.cuboid(crossbar.x() * crossbarScale, crossbar.y(), z, crossbar.width() * crossbarScale,
crossbar.height(), crossbar.depth()),
ModelTransform.NONE);
}
}
return TexturedModelData.of(model, 2048, 1024).createModel();
}
}