Instancer interface

- Entire material/model system finally cleaned up
This commit is contained in:
Jozufozu 2021-09-12 09:40:36 -07:00
parent 7e65eaa00d
commit aa83cd5c60
12 changed files with 361 additions and 324 deletions

View file

@ -0,0 +1,325 @@
package com.jozufozu.flywheel.backend.instancing;
import java.util.ArrayList;
import java.util.BitSet;
import com.jozufozu.flywheel.backend.Backend;
import com.jozufozu.flywheel.backend.gl.GlVertexArray;
import com.jozufozu.flywheel.backend.gl.attrib.VertexFormat;
import com.jozufozu.flywheel.backend.gl.buffer.GlBuffer;
import com.jozufozu.flywheel.backend.gl.buffer.GlBufferType;
import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer;
import com.jozufozu.flywheel.backend.model.ModelAllocator;
import com.jozufozu.flywheel.backend.model.IBufferedModel;
import com.jozufozu.flywheel.core.model.IModel;
import com.jozufozu.flywheel.util.AttribUtil;
/**
* An instancer is how you interact with an instanced model.
* <p>
* Instanced models can have many copies, and on most systems it's very fast to draw all of the copies at once.
* There is no limit to how many copies an instanced model can have.
* Each copy is represented by an InstanceData object.
* </p>
* <p>
* When you call {@link #createInstance()} you are given an InstanceData object that you can manipulate however
* you want. The changes you make to the InstanceData object are automatically made visible, and persistent.
* Changing the position of your InstanceData object every frame means that that copy of the model will be in a
* different position in the world each frame. Setting the position of your InstanceData once and not touching it
* again means that your model will be in the same position in the world every frame. This persistence is useful
* because it means the properties of your model don't have to be re-evaluated every frame.
* </p>
*
* @param <D> the data that represents a copy of the instanced model.
*/
public class GPUInstancer<D extends InstanceData> implements Instancer<D> {
private final ModelAllocator modelAllocator;
private final IModel modelData;
private final VertexFormat instanceFormat;
private final IInstanceFactory<D> factory;
private IBufferedModel model;
private GlVertexArray vao;
private GlBuffer instanceVBO;
private int glBufferSize = -1;
private int glInstanceCount = 0;
private boolean deleted;
private boolean initialized;
private final ArrayList<D> data = new ArrayList<>();
boolean anyToRemove;
boolean anyToUpdate;
public GPUInstancer(ModelAllocator modelAllocator, IModel model, IInstanceFactory<D> factory, VertexFormat instanceFormat) {
this.modelAllocator = modelAllocator;
this.modelData = model;
this.factory = factory;
this.instanceFormat = instanceFormat;
}
/**
* @return a handle to a new copy of this model.
*/
@Override
public D createInstance() {
return _add(factory.create(this));
}
/**
* Copy a data from another Instancer to this.
*
* This has the effect of swapping out one model for another.
* @param inOther the data associated with a different model.
*/
@Override
public void stealInstance(D inOther) {
if (inOther.owner == this) return;
inOther.delete();
// sike, we want to keep it, changing the owner reference will still delete it in the other
inOther.removed = false;
_add(inOther);
}
public void render() {
if (invalid()) return;
vao.bind();
renderSetup();
if (glInstanceCount > 0) model.drawInstances(glInstanceCount);
// persistent mapping sync point
instanceVBO.doneForThisFrame();
vao.unbind();
}
private boolean invalid() {
return deleted || model == null;
}
public void init() {
if (isInitialized()) return;
initialized = true;
vao = new GlVertexArray();
model = modelAllocator.alloc(modelData, arenaModel -> {
vao.bind();
model.setupState();
vao.unbind();
});
vao.bind();
instanceVBO = GlBuffer.requestPersistent(GlBufferType.ARRAY_BUFFER);
AttribUtil.enableArrays(model.getAttributeCount() + instanceFormat.getAttributeCount());
vao.unbind();
}
public boolean isInitialized() {
return initialized;
}
public boolean isEmpty() {
return !anyToUpdate && !anyToRemove && glInstanceCount == 0;
}
/**
* Clear all instance data without freeing resources.
*/
public void clear() {
data.clear();
anyToRemove = true;
}
/**
* Free acquired resources. All other Instancer methods are undefined behavior after calling delete.
*/
public void delete() {
if (invalid()) return;
deleted = true;
model.delete();
instanceVBO.delete();
vao.delete();
}
private D _add(D instanceData) {
instanceData.owner = this;
instanceData.dirty = true;
anyToUpdate = true;
synchronized (data) {
data.add(instanceData);
}
return instanceData;
}
protected void renderSetup() {
if (anyToRemove) {
removeDeletedInstances();
}
instanceVBO.bind();
if (!realloc()) {
if (anyToRemove) {
clearBufferTail();
}
if (anyToUpdate) {
updateBuffer();
}
glInstanceCount = data.size();
}
instanceVBO.unbind();
anyToRemove = anyToUpdate = false;
}
private void clearBufferTail() {
int size = data.size();
final int offset = size * instanceFormat.getStride();
final int length = glBufferSize - offset;
if (length > 0) {
instanceVBO.getBuffer(offset, length)
.putByteArray(new byte[length])
.flush();
}
}
private void updateBuffer() {
final int size = data.size();
if (size <= 0) return;
final int stride = instanceFormat.getStride();
final BitSet dirtySet = getDirtyBitSet();
if (dirtySet.isEmpty()) return;
final int firstDirty = dirtySet.nextSetBit(0);
final int lastDirty = dirtySet.previousSetBit(size);
final int offset = firstDirty * stride;
final int length = (1 + lastDirty - firstDirty) * stride;
if (length > 0) {
MappedBuffer mapped = instanceVBO.getBuffer(offset, length);
dirtySet.stream()
.forEach(i -> {
final D d = data.get(i);
mapped.position(i * stride);
d.write(mapped);
});
mapped.flush();
}
}
private BitSet getDirtyBitSet() {
final int size = data.size();
final BitSet dirtySet = new BitSet(size);
for (int i = 0; i < size; i++) {
D element = data.get(i);
if (element.dirty) {
dirtySet.set(i);
element.dirty = false;
}
}
return dirtySet;
}
private boolean realloc() {
int size = this.data.size();
int stride = instanceFormat.getStride();
int requiredSize = size * stride;
if (requiredSize > glBufferSize) {
glBufferSize = requiredSize + stride * 16;
instanceVBO.alloc(glBufferSize);
MappedBuffer buffer = instanceVBO.getBuffer(0, glBufferSize);
for (D datum : data) {
datum.write(buffer);
}
buffer.flush();
glInstanceCount = size;
informAttribDivisors();
return true;
}
return false;
}
private void removeDeletedInstances() {
// Figure out which elements are to be removed.
final int oldSize = this.data.size();
int removeCount = 0;
final BitSet removeSet = new BitSet(oldSize);
for (int i = 0; i < oldSize; i++) {
final D element = this.data.get(i);
if (element.removed || element.owner != this) {
removeSet.set(i);
removeCount++;
}
}
final int newSize = oldSize - removeCount;
// shift surviving elements left over the spaces left by removed elements
for (int i = 0, j = 0; (i < oldSize) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
if (i != j) {
D element = data.get(i);
data.set(j, element);
element.dirty = true;
}
}
anyToUpdate = true;
data.subList(newSize, oldSize)
.clear();
}
private void informAttribDivisors() {
int staticAttributes = model.getAttributeCount();
instanceFormat.vertexAttribPointers(staticAttributes);
for (int i = 0; i < instanceFormat.getAttributeCount(); i++) {
Backend.getInstance().compat.instancedArrays.vertexAttribDivisor(i + staticAttributes, 1);
}
}
@Override
public void markDirty(InstanceData instanceData) {
anyToUpdate = true;
instanceData.dirty = true;
}
@Override
public void markRemoval(InstanceData instanceData) {
anyToRemove = true;
instanceData.removed = true;
}
}

View file

@ -16,13 +16,11 @@ public abstract class InstanceData {
public abstract void write(MappedBuffer buf);
public void markDirty() {
owner.anyToUpdate = true;
dirty = true;
owner.markDirty(this);
}
public void delete() {
owner.anyToRemove = true;
removed = true;
owner.markRemoval(this);
}
}

View file

@ -1,71 +1,10 @@
package com.jozufozu.flywheel.backend.instancing;
import java.util.ArrayList;
import java.util.BitSet;
import com.jozufozu.flywheel.backend.Backend;
import com.jozufozu.flywheel.backend.gl.GlVertexArray;
import com.jozufozu.flywheel.backend.gl.attrib.VertexFormat;
import com.jozufozu.flywheel.backend.gl.buffer.GlBuffer;
import com.jozufozu.flywheel.backend.gl.buffer.GlBufferType;
import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer;
import com.jozufozu.flywheel.backend.material.MaterialSpec;
import com.jozufozu.flywheel.backend.model.ModelAllocator;
import com.jozufozu.flywheel.backend.model.IBufferedModel;
import com.jozufozu.flywheel.core.model.IModel;
import com.jozufozu.flywheel.util.AttribUtil;
/**
* An instancer is how you interact with an instanced model.
* <p>
* Instanced models can have many copies, and on most systems it's very fast to draw all of the copies at once.
* There is no limit to how many copies an instanced model can have.
* Each copy is represented by an InstanceData object.
* </p>
* <p>
* When you call {@link #createInstance()} you are given an InstanceData object that you can manipulate however
* you want. The changes you make to the InstanceData object are automatically made visible, and persistent.
* Changing the position of your InstanceData object every frame means that that copy of the model will be in a
* different position in the world each frame. Setting the position of your InstanceData once and not touching it
* again means that your model will be in the same position in the world every frame. This persistence is useful
* because it means the properties of your model don't have to be re-evaluated every frame.
* </p>
*
* @param <D> the data that represents a copy of the instanced model.
*/
public class Instancer<D extends InstanceData> {
private final ModelAllocator modelAllocator;
private final IModel modelData;
private final VertexFormat instanceFormat;
private final IInstanceFactory<D> factory;
private IBufferedModel model;
private GlVertexArray vao;
private GlBuffer instanceVBO;
private int glBufferSize = -1;
private int glInstanceCount = 0;
private boolean deleted;
private boolean initialized;
private final ArrayList<D> data = new ArrayList<>();
boolean anyToRemove;
boolean anyToUpdate;
public Instancer(ModelAllocator modelAllocator, IModel model, IInstanceFactory<D> factory, VertexFormat instanceFormat) {
this.modelAllocator = modelAllocator;
this.modelData = model;
this.factory = factory;
this.instanceFormat = instanceFormat;
}
public interface Instancer<D extends InstanceData> {
/**
* @return a handle to a new copy of this model.
*/
public D createInstance() {
return _add(factory.create(this));
}
D createInstance();
/**
* Copy a data from another Instancer to this.
@ -73,239 +12,9 @@ public class Instancer<D extends InstanceData> {
* This has the effect of swapping out one model for another.
* @param inOther the data associated with a different model.
*/
public void stealInstance(D inOther) {
if (inOther.owner == this) return;
void stealInstance(D inOther);
inOther.owner.anyToRemove = true;
_add(inOther);
}
public void render() {
if (invalid()) return;
vao.bind();
renderSetup();
if (glInstanceCount > 0) model.drawInstances(glInstanceCount);
// persistent mapping sync point
instanceVBO.doneForThisFrame();
vao.unbind();
}
private boolean invalid() {
return deleted || model == null;
}
public void init() {
if (isInitialized()) return;
initialized = true;
vao = new GlVertexArray();
model = modelAllocator.alloc(modelData, arenaModel -> {
vao.bind();
model.setupState();
vao.unbind();
});
vao.bind();
instanceVBO = GlBuffer.requestPersistent(GlBufferType.ARRAY_BUFFER);
AttribUtil.enableArrays(model.getAttributeCount() + instanceFormat.getAttributeCount());
vao.unbind();
}
public boolean isInitialized() {
return initialized;
}
public boolean isEmpty() {
return !anyToUpdate && !anyToRemove && glInstanceCount == 0;
}
/**
* Clear all instance data without freeing resources.
*/
public void clear() {
data.clear();
anyToRemove = true;
}
/**
* Free acquired resources. All other Instancer methods are undefined behavior after calling delete.
*/
public void delete() {
if (invalid()) return;
deleted = true;
model.delete();
instanceVBO.delete();
vao.delete();
}
private D _add(D instanceData) {
instanceData.owner = this;
instanceData.dirty = true;
anyToUpdate = true;
synchronized (data) {
data.add(instanceData);
}
return instanceData;
}
protected void renderSetup() {
if (anyToRemove) {
removeDeletedInstances();
}
instanceVBO.bind();
if (!realloc()) {
if (anyToRemove) {
clearBufferTail();
}
if (anyToUpdate) {
updateBuffer();
}
glInstanceCount = data.size();
}
instanceVBO.unbind();
anyToRemove = anyToUpdate = false;
}
private void clearBufferTail() {
int size = data.size();
final int offset = size * instanceFormat.getStride();
final int length = glBufferSize - offset;
if (length > 0) {
instanceVBO.getBuffer(offset, length)
.putByteArray(new byte[length])
.flush();
}
}
private void updateBuffer() {
final int size = data.size();
if (size <= 0) return;
final int stride = instanceFormat.getStride();
final BitSet dirtySet = getDirtyBitSet();
if (dirtySet.isEmpty()) return;
final int firstDirty = dirtySet.nextSetBit(0);
final int lastDirty = dirtySet.previousSetBit(size);
final int offset = firstDirty * stride;
final int length = (1 + lastDirty - firstDirty) * stride;
if (length > 0) {
MappedBuffer mapped = instanceVBO.getBuffer(offset, length);
dirtySet.stream()
.forEach(i -> {
final D d = data.get(i);
mapped.position(i * stride);
d.write(mapped);
});
mapped.flush();
}
}
private BitSet getDirtyBitSet() {
final int size = data.size();
final BitSet dirtySet = new BitSet(size);
for (int i = 0; i < size; i++) {
D element = data.get(i);
if (element.dirty) {
dirtySet.set(i);
element.dirty = false;
}
}
return dirtySet;
}
private boolean realloc() {
int size = this.data.size();
int stride = instanceFormat.getStride();
int requiredSize = size * stride;
if (requiredSize > glBufferSize) {
glBufferSize = requiredSize + stride * 16;
instanceVBO.alloc(glBufferSize);
MappedBuffer buffer = instanceVBO.getBuffer(0, glBufferSize);
for (D datum : data) {
datum.write(buffer);
}
buffer.flush();
glInstanceCount = size;
informAttribDivisors();
return true;
}
return false;
}
private void removeDeletedInstances() {
// Figure out which elements are to be removed.
final int oldSize = this.data.size();
int removeCount = 0;
final BitSet removeSet = new BitSet(oldSize);
for (int i = 0; i < oldSize; i++) {
final D element = this.data.get(i);
if (element.removed || element.owner != this) {
removeSet.set(i);
removeCount++;
}
}
final int newSize = oldSize - removeCount;
// shift surviving elements left over the spaces left by removed elements
for (int i = 0, j = 0; (i < oldSize) && (j < newSize); i++, j++) {
i = removeSet.nextClearBit(i);
if (i != j) {
D element = data.get(i);
data.set(j, element);
element.dirty = true;
}
}
anyToUpdate = true;
data.subList(newSize, oldSize)
.clear();
}
private void informAttribDivisors() {
int staticAttributes = model.getAttributeCount();
instanceFormat.vertexAttribPointers(staticAttributes);
for (int i = 0; i < instanceFormat.getAttributeCount(); i++) {
Backend.getInstance().compat.instancedArrays.vertexAttribDivisor(i + staticAttributes, 1);
}
}
void markDirty(InstanceData instanceData);
void markRemoval(InstanceData instanceData);
}

View file

@ -3,7 +3,7 @@ package com.jozufozu.flywheel.backend.instancing.tile;
import com.jozufozu.flywheel.backend.instancing.IDynamicInstance;
import com.jozufozu.flywheel.backend.instancing.AbstractInstance;
import com.jozufozu.flywheel.backend.instancing.ITickableInstance;
import com.jozufozu.flywheel.backend.material.InstanceMaterial;
import com.jozufozu.flywheel.backend.material.Material;
import com.jozufozu.flywheel.backend.material.MaterialManager;
import com.jozufozu.flywheel.core.Materials;
import com.jozufozu.flywheel.core.materials.ModelData;
@ -76,11 +76,11 @@ public abstract class TileEntityInstance<T extends TileEntity> extends AbstractI
return pos;
}
protected InstanceMaterial<ModelData> getTransformMaterial() {
protected Material<ModelData> getTransformMaterial() {
return materialManager.defaultCutout().material(Materials.TRANSFORMED);
}
protected InstanceMaterial<OrientedData> getOrientedMaterial() {
protected Material<OrientedData> getOrientedMaterial() {
return materialManager.defaultCutout().material(Materials.ORIENTED);
}

View file

@ -14,7 +14,7 @@ import com.mojang.blaze3d.matrix.MatrixStack;
import net.minecraft.block.BlockState;
import net.minecraft.util.Direction;
public interface InstanceMaterial<D extends InstanceData> {
public interface Material<D extends InstanceData> {
/**
* Get an instancer for the given model. Calling this method twice with the same key will return the same instancer.
*

View file

@ -10,5 +10,5 @@ public interface MaterialGroup {
* @param <D> The type representing the per instance data.
* @return A material you can use to render models.
*/
<D extends InstanceData> InstanceMaterial<D> material(MaterialSpec<D> spec);
<D extends InstanceData> Material<D> material(MaterialSpec<D> spec);
}

View file

@ -23,7 +23,7 @@ public class MaterialGroupImpl<P extends WorldProgram> implements MaterialGroup
private final ArrayList<MaterialRenderer<P>> renderers = new ArrayList<>();
private final Map<MaterialSpec<?>, InstanceMaterialImpl<?>> materials = new HashMap<>();
private final Map<MaterialSpec<?>, MaterialImpl<?>> materials = new HashMap<>();
public MaterialGroupImpl(MaterialManagerImpl<P> owner, IRenderState state) {
this.owner = owner;
@ -38,8 +38,8 @@ public class MaterialGroupImpl<P extends WorldProgram> implements MaterialGroup
*/
@SuppressWarnings("unchecked")
@Override
public <D extends InstanceData> InstanceMaterialImpl<D> material(MaterialSpec<D> spec) {
return (InstanceMaterialImpl<D>) materials.computeIfAbsent(spec, this::createInstanceMaterial);
public <D extends InstanceData> MaterialImpl<D> material(MaterialSpec<D> spec) {
return (MaterialImpl<D>) materials.computeIfAbsent(spec, this::createInstanceMaterial);
}
public void render(Matrix4f viewProjection, double camX, double camY, double camZ) {
@ -53,19 +53,19 @@ public class MaterialGroupImpl<P extends WorldProgram> implements MaterialGroup
}
public void clear() {
materials.values().forEach(InstanceMaterialImpl::clear);
materials.values().forEach(MaterialImpl::clear);
}
public void delete() {
materials.values()
.forEach(InstanceMaterialImpl::delete);
.forEach(MaterialImpl::delete);
materials.clear();
renderers.clear();
}
private InstanceMaterialImpl<?> createInstanceMaterial(MaterialSpec<?> type) {
InstanceMaterialImpl<?> material = new InstanceMaterialImpl<>(type);
private MaterialImpl<?> createInstanceMaterial(MaterialSpec<?> type) {
MaterialImpl<?> material = new MaterialImpl<>(type);
this.renderers.add(new MaterialRenderer<>(owner.getProgram(type.getProgramName()), material, this::setup));

View file

@ -6,6 +6,7 @@ import java.util.function.Supplier;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.jozufozu.flywheel.backend.RenderWork;
import com.jozufozu.flywheel.backend.instancing.GPUInstancer;
import com.jozufozu.flywheel.backend.instancing.InstanceData;
import com.jozufozu.flywheel.backend.instancing.Instancer;
import com.jozufozu.flywheel.backend.model.ModelPool;
@ -15,19 +16,19 @@ import com.jozufozu.flywheel.core.model.IModel;
* A collection of Instancers that all have the same format.
* @param <D>
*/
public class InstanceMaterialImpl<D extends InstanceData> implements InstanceMaterial<D> {
public class MaterialImpl<D extends InstanceData> implements Material<D> {
final ModelPool modelPool;
protected final Cache<Object, Instancer<D>> models;
protected final Cache<Object, GPUInstancer<D>> models;
protected final MaterialSpec<D> spec;
public InstanceMaterialImpl(MaterialSpec<D> spec) {
public MaterialImpl(MaterialSpec<D> spec) {
this.spec = spec;
modelPool = new ModelPool(spec.getModelFormat(), spec.getModelFormat().getStride() * 64);
this.models = CacheBuilder.newBuilder()
.removalListener(notification -> {
Instancer<?> instancer = (Instancer<?>) notification.getValue();
GPUInstancer<?> instancer = (GPUInstancer<?>) notification.getValue();
RenderWork.enqueue(instancer::delete);
})
.build();
@ -43,7 +44,7 @@ public class InstanceMaterialImpl<D extends InstanceData> implements InstanceMat
@Override
public Instancer<D> model(Object key, Supplier<IModel> modelSupplier) {
try {
return models.get(key, () -> new Instancer<>(modelPool, modelSupplier.get(), spec.getInstanceFactory(), spec.getInstanceFormat()));
return models.get(key, () -> new GPUInstancer<>(modelPool, modelSupplier.get(), spec.getInstanceFactory(), spec.getInstanceFormat()));
} catch (ExecutionException e) {
throw new RuntimeException("error creating instancer", e);
}
@ -53,7 +54,7 @@ public class InstanceMaterialImpl<D extends InstanceData> implements InstanceMat
return models.size() > 0 && models.asMap()
.values()
.stream()
.allMatch(Instancer::isEmpty);
.allMatch(GPUInstancer::isEmpty);
}
public void delete() {
@ -67,7 +68,7 @@ public class InstanceMaterialImpl<D extends InstanceData> implements InstanceMat
public void clear() {
models.asMap()
.values()
.forEach(Instancer::clear);
.forEach(GPUInstancer::clear);
}
}

View file

@ -4,6 +4,7 @@ import java.util.Collection;
import java.util.function.Consumer;
import java.util.function.Supplier;
import com.jozufozu.flywheel.backend.instancing.GPUInstancer;
import com.jozufozu.flywheel.backend.instancing.Instancer;
import com.jozufozu.flywheel.core.shader.WorldProgram;
@ -12,11 +13,11 @@ import net.minecraft.util.math.vector.Matrix4f;
public class MaterialRenderer<P extends WorldProgram> {
protected final Supplier<P> program;
protected final InstanceMaterialImpl<?> material;
protected final MaterialImpl<?> material;
protected final Consumer<P> setupFunc;
public MaterialRenderer(Supplier<P> programSupplier, InstanceMaterialImpl<?> material, Consumer<P> setupFunc) {
public MaterialRenderer(Supplier<P> programSupplier, MaterialImpl<?> material, Consumer<P> setupFunc) {
this.program = programSupplier;
this.material = material;
this.setupFunc = setupFunc;
@ -25,11 +26,11 @@ public class MaterialRenderer<P extends WorldProgram> {
public void render(Matrix4f viewProjection, double camX, double camY, double camZ) {
if (material.nothingToRender()) return;
Collection<? extends Instancer<?>> instancers = material.models.asMap()
Collection<? extends GPUInstancer<?>> instancers = material.models.asMap()
.values();
// initialize all uninitialized instancers...
instancers.forEach(Instancer::init);
instancers.forEach(GPUInstancer::init);
// ...and then flush the model arena in case anything was marked for upload
material.modelPool.flush();
@ -41,7 +42,7 @@ public class MaterialRenderer<P extends WorldProgram> {
setupFunc.accept(program);
instancers.forEach(Instancer::render);
instancers.forEach(GPUInstancer::render);
}
}

View file

@ -1,6 +1,7 @@
package com.jozufozu.flywheel.core.materials;
import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer;
import com.jozufozu.flywheel.backend.instancing.GPUInstancer;
import com.jozufozu.flywheel.backend.instancing.InstanceData;
import com.jozufozu.flywheel.backend.instancing.Instancer;

View file

@ -1,6 +1,7 @@
package com.jozufozu.flywheel.core.materials;
import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer;
import com.jozufozu.flywheel.backend.instancing.GPUInstancer;
import com.jozufozu.flywheel.backend.instancing.Instancer;
import com.jozufozu.flywheel.util.RenderUtil;
import com.mojang.blaze3d.matrix.MatrixStack;

View file

@ -1,6 +1,7 @@
package com.jozufozu.flywheel.core.materials;
import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer;
import com.jozufozu.flywheel.backend.instancing.GPUInstancer;
import com.jozufozu.flywheel.backend.instancing.Instancer;
import com.jozufozu.flywheel.util.vec.Vec3;