Difference between revisions of "Cached Smart Models"

From McJty Modding
Jump to: navigation, search
m (1 revision imported)
 
m (1 revision imported)
 
(No difference)

Latest revision as of 21:07, 19 November 2017

This is a small suite of utility classes I made to make my life a bit easier as I'm making large quantities of smart block models, hopefully you find them useful too. Theoretically they could be ported to 1.9 with little to no change to the models. The only major changes that would be required is shifting some stuff around in the ISmartBlockModel to make it an IBlockModel.

Using the library

Really, once you've copied over the classes to your mod and added the necessary information (I put syntax errors so it'll be easy to spot) you just have to implement it.

Registering

Do this on the client side only, and only once.

Register the textures that will be used by the renderer:

ModelHandler.INSTANCE.registerTexture("blocks/myfolder/texture");

Set the block to always use the exact same model:

ModelHandler.setStaticMap(blockInstance, "blockName");

Register the model definition:

// If you are using java 7 or earlier do this
ModelHandler.INSTANCE.registerModel("blockName", new Supplier<IBakedModel>() {

    @Override
    public IBakedModel get() {
        return new CachedSmartModel(new MyModelDefinition());
    }
    
});

// If you are using java 8 or later you can use this instead
ModelHandler.INSTANCE.registerModel("blockName", () -> new CachedSmartModel(new MyModelDefinition()) );

Model Definition

[Error: put your package here];

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import catwalks.Const;
import catwalks.block.EnumCatwalkMaterial;
import catwalks.render.ModelUtils;
import catwalks.render.ModelUtils.SpritelessQuad;
import catwalks.render.cached.SimpleModel;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.common.property.IExtendedBlockState;

public class CatwalkModel extends SimpleModel {

    @Override
    public List<Object> getKey(IBlockState rawstate) {
        IExtendedBlockState state = (IExtendedBlockState) rawstate; // do this if you are using an extended state

        return Arrays.asList(new Object[]{ // create a list that holds all the values you need for the renderer
                state.getValue(TheBlockClass.PropertyOne),
                state.getValue(TheBlockClass.PropertyTwo),
                state.getValue(TheBlockClass.PropertyThree),
                state.getValue(TheBlockClass.PropertyFour),
                ... etc.
        });
        
    }

    @Override
    protected List<BakedQuad> generateQuads(List<Object> list) {
        int i = 0;
        
        // turn the list into useful values
        SomeType  propOne   = (SomeType) list.get(i++); // get i and increment i for the next time
        SomeOther propTwo   = (SomeOther) list.get(i++);
        boolean   propThree = (boolean) list.get(i++),
                  propFour  = (boolean) list.get(i++);
        
        // Get the textures, I believe this can't happen in the constructor or something like that
        TextureAtlasSprite
            tex     = ModelUtils.getSprite( new ResourceLocation(YOUR_MODID + ":blocks/myfolder/texture") ),
            overlay = ModelUtils.getSprite( new ResourceLocation(Const.MODID + ":blocks/myfolder/textureOverlay") );
        
        // A list of quads that haven't been mapped to a texture yet
        List<SpritelessQuad> sideQuads = new ArrayList<>();

        // Add quads, more methods exist in ModelUtils to provide more fine grained control.
        ModelUtils.putFace(sideQuads, EnumFacing.NORTH, 0);
        ModelUtils.putFace(sideQuads, EnumFacing.SOUTH, 1);
        ModelUtils.putFace(sideQuads, EnumFacing.EAST,  2);
        // Add another face. A condition ID of -1 will always be true, and any other out of bounds values will always be false
        ModelUtils.putFace(sideQuads, EnumFacing.DOWN, -1);
        
        // Create a list to hold the final output quads
        List<BakedQuad> quads = new ArrayList<>();

        // Process the quads by mapping their uv coordinates onto the passed texture,
        // condition 0 will be the 'north' boolean value, 1 will be 'south', and so on
        ModelUtils.processConditionalQuads(sideQuads, quads, tex, north, south, east);

        // Process the quads again using a different texture, but only if the overlay flag is true
        // These quads won't z-fight as they are exactly the same as the others.
        if(overlay) ModelUtils.processConditionalQuads(sideQuads, quads, overlay, north, south, east);

        // Return the quads to be cached
        return quads;
    }

}

And that's it! If all goes well you should have an easy to make model that's generated using code and cached so it's much faster.

Library code:

I've put text that will throw a syntax error into the code where you need to fill in stuff. For the packages I have it like so:

  • > render
    • ModelHandler.java
    • ModelUtils.java
    • > cached
      • CachedSmartModel.java
      • SimpleModel.java
      • > models
        • [ all my actual model definitions ]

CachedSmartModel

The 1.8.9 ISmartBlockModel implementation

[Error: put your package here]

import java.util.List;
import java.util.concurrent.ExecutionException;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;

import catwalks.render.ModelUtils;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.block.model.ItemCameraTransforms;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.resources.model.IBakedModel;
import net.minecraft.util.EnumFacing;
import net.minecraftforge.client.model.ISmartBlockModel;

@SuppressWarnings("deprecation")
public class CachedSmartModel implements ISmartBlockModel {

    LoadingCache<List<Object>, IBakedModel> modelCache;
    SimpleModel model;
    
    public CachedSmartModel(SimpleModel model) {
        this.model = model;
        modelCache = CacheBuilder.newBuilder().build(new CacheLoader<List<Object>, IBakedModel>() {

            @Override
            public IBakedModel load(List<Object> key) throws Exception {
                return new BakedModelCache(model.getQuads(key), model.getParticleSprite(key));
            }
            
        });
    }
    
    @Override
    public IBakedModel handleBlockState(IBlockState state) {
        try {
            return modelCache.get(model.getKey(state));
        } catch (ExecutionException e) {
            e.printStackTrace();
            return BakedModelCache.NULL;
        }
    }
    
    public static class BakedModelCache implements IBakedModel {
        
        public static final IBakedModel NULL = new BakedModelCache(
            ImmutableList.of(), ModelUtils.getSprite( TextureMap.LOCATION_MISSING_TEXTURE )
        );
        
        List<List<BakedQuad>> quads;
        TextureAtlasSprite particleTexture;
        
        public BakedModelCache(List<List<BakedQuad>> quads, TextureAtlasSprite particleTexture) {
            this.quads = quads;
            this.particleTexture = particleTexture;
        }
        
        @Override
        public List<BakedQuad> getFaceQuads(EnumFacing side) {
            int index = side.getIndex();
            if(index < quads.size())
                return quads.get(index);
            else
                return ImmutableList.of();
        }

        @Override
        public List<BakedQuad> getGeneralQuads() {
            int index = EnumFacing.values().length;
            if(index < quads.size())
                return quads.get(index);
            else
                return ImmutableList.of();
        }

        @Override
        public boolean isAmbientOcclusion() {
            return true;
        }

        @Override
        public boolean isGui3d() {
            return false;
        }

        @Override
        public boolean isBuiltInRenderer() {
            return false;
        }

        @Override
        public TextureAtlasSprite getParticleTexture() {
            return particleTexture;
        }

        @Override
        public ItemCameraTransforms getItemCameraTransforms() {
            return null;
        }
        
    }

    @Override
    public List<BakedQuad> getFaceQuads(EnumFacing p_177551_1_) { return null; }

    @Override
    public List<BakedQuad> getGeneralQuads() { return null; }

    @Override
    public boolean isAmbientOcclusion() { return false; }

    @Override
    public boolean isGui3d() { return false; }

    @Override
    public boolean isBuiltInRenderer() { return false; }

    @Override
    public TextureAtlasSprite getParticleTexture() { return ModelUtils.getSprite( TextureMap.LOCATION_MISSING_TEXTURE ); }

    @Override
    public ItemCameraTransforms getItemCameraTransforms() { return null; }

}

SimpleModel

The abstract model class

[Error: put your package here]

import java.util.ArrayList;
import java.util.List;

import com.google.common.collect.ImmutableList;

import catwalks.render.ModelUtils;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.util.EnumFacing;

public abstract class SimpleModel {
    
    public List<List<BakedQuad>> getQuads(List<Object> key) {
        List<List<BakedQuad>> quads = new ArrayList<>();
        List<BakedQuad> rawQuads = generateQuads(key);
        
        for (int i = 0; i < EnumFacing.VALUES.length+1; i++) {
            EnumFacing side = null;
            if(i < EnumFacing.VALUES.length)
                side = EnumFacing.VALUES[i];
            List<BakedQuad> sideQuads = new ArrayList<>();
            for (BakedQuad quad : rawQuads) {
                if(quad.getFace() == side)
                    sideQuads.add(quad);
            }
            quads.add(ImmutableList.copyOf(sideQuads));
        }
        return ImmutableList.copyOf(quads);
    }
    
    public TextureAtlasSprite getParticleSprite(List<Object> key) {
        return ModelUtils.getSprite( TextureMap.LOCATION_MISSING_TEXTURE );
    }
    
    public abstract List<Object> getKey(IBlockState state);
    
    protected abstract List<BakedQuad> generateQuads(List<Object> list);

}

ModelUtils

Making models easy™

[Error: put your package here]

import java.util.List;

import com.google.common.primitives.Ints;

import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.Vec3;

public class ModelUtils {

    public static class SpritelessQuad {
        public Vec3 p1, p2, p3, p4;
        public float u1, v1, u2, v2, u3, v3, u4, v4;

        public EnumFacing side;
        public int conditionID;

        public SpritelessQuad( int condition,
                Vec3 p1, float u1, float v1,
                Vec3 p2, float u2, float v2,
                Vec3 p3, float u3, float v3,
                Vec3 p4, float u4, float v4,
                EnumFacing side) {

            this.conditionID = condition;

            this.p1 = p1;
            this.u1 = u1;
            this.v1 = v1;

            this.p2 = p2;
            this.u2 = u2;
            this.v2 = v2;

            this.p3 = p3;
            this.u3 = u3;
            this.v3 = v3;

            this.p4 = p4;
            this.u4 = u4;
            this.v4 = v4;

            this.side = side;
        }

        public BakedQuad bakedQuad(TextureAtlasSprite sprite) {
            return new BakedQuad(Ints.concat(
                vertexToInts(p1.xCoord, p1.yCoord, p1.zCoord, u1, v1, sprite),
                vertexToInts(p2.xCoord, p2.yCoord, p2.zCoord, u2, v2, sprite),
                vertexToInts(p3.xCoord, p3.yCoord, p3.zCoord, u3, v3, sprite),
                vertexToInts(p4.xCoord, p4.yCoord, p4.zCoord, u4, v4, sprite)
            ), -1, side);
        }
    }

    /**
     * Get the sprite from the texture sheet for the specified location.
     * @param location
     * @return
     */
    public static TextureAtlasSprite getSprite(ResourceLocation location) {
        return Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(location.toString());
    }

    /**
     * Creates a quad and it's inverse so it can be seen from both sides, UV values span the entire sprite
     * @param quads The list of quads to add to
     * @param cull The side to cull the face, null for no culling
     * @param condition The condition ID for enabling this quad
     * @param v1 Top left uv(0,0)
     * @param v2 Top right uv(1,0)
     * @param v3 Bottom right uv(1,1)
     * @param v4 Bottom left uv(0,1)
     */
    public static void fullSpriteDoubleQuad(List<SpritelessQuad> quads, EnumFacing cull, int condition, Vec3 v1, Vec3 v2, Vec3 v3, Vec3 v4) {
        doubleQuad(quads, cull, condition,
            v1.xCoord, v1.yCoord, v1.zCoord, 0, 0,
            v2.xCoord, v2.yCoord, v2.zCoord, 1, 0,
            v3.xCoord, v3.yCoord, v3.zCoord, 1, 1,
            v4.xCoord, v4.yCoord, v4.zCoord, 0, 1
        );
    }

    /**
     * Creates a quad and it's inverse so it can be seen from both sides
     * @param quads The list of quads to add to
     * @param side The side to cull the face, null for no culling
     * @param condition The condition ID for enabling this quad
     */
    public static void doubleQuad(List<SpritelessQuad> quads, EnumFacing side, int condition,
            double x1, double y1, double z1, double u1, double v1,
            double x2, double y2, double z2, double u2, double v2,
            double x3, double y3, double z3, double u3, double v3,
            double x4, double y4, double z4, double u4, double v4) {

        quads.add(new SpritelessQuad(condition,
            new Vec3(x1,y1,z1), (float)u1*16, (float)v1*16,
            new Vec3(x2,y2,z2), (float)u2*16, (float)v2*16,
            new Vec3(x3,y3,z3), (float)u3*16, (float)v3*16,
            new Vec3(x4,y4,z4), (float)u4*16, (float)v4*16,
            side));  // v1, v2, v3, v4
        quads.add(new SpritelessQuad(condition,
            new Vec3(x4,y4,z4), (float)u4*16, (float)v4*16,
            new Vec3(x3,y3,z3), (float)u3*16, (float)v3*16,
            new Vec3(x2,y2,z2), (float)u2*16, (float)v2*16,
            new Vec3(x1,y1,z1), (float)u1*16, (float)v1*16,
            side));  // v4, v3, v2, v1 (reverse order, face is flipped)
    }

    /**
     * Creates a quad without it's inverse, meaning it's only visible from one side
     * @param quads The list of quads to add to
     * @param side The side to cull the face, null for no culling
     * @param condition The condition ID for enabling this quad
     */
    public static void singleQuad(List<SpritelessQuad> quads, EnumFacing side, int condition,
            double x1, double y1, double z1, double u1, double v1,
            double x2, double y2, double z2, double u2, double v2,
            double x3, double y3, double z3, double u3, double v3,
            double x4, double y4, double z4, double u4, double v4) {

        quads.add(new SpritelessQuad(condition,
            new Vec3(x1,y1,z1), (float)u1*16, (float)v1*16,
            new Vec3(x2,y2,z2), (float)u2*16, (float)v2*16,
            new Vec3(x3,y3,z3), (float)u3*16, (float)v3*16,
            new Vec3(x4,y4,z4), (float)u4*16, (float)v4*16,
            side));  // v1, v2, v3, v4
    }

    /**
     * Put the conditional quads into the baked quads list, using the supplied sprite and conditions.
     * @param rawQuads List of spriteless quads to be processed
     * @param quads List for final quads to be inserted into
     * @param sprite TextureAtlasSprite to use
     * @param conditions List of conditions
     */
    public static void processConditionalQuads(List<SpritelessQuad> rawQuads, List<BakedQuad> quads, TextureAtlasSprite sprite, boolean... conditions) {
        for (SpritelessQuad quad : rawQuads) {
            if(quad.conditionID == -1 || ( quad.conditionID < conditions.length && conditions[quad.conditionID]) ) {
                // -1 is always true, anything else that's out of bounds is false
                quads.add(quad.bakedQuad(sprite));
            }
        }
    }

    /**
     * Creates a full sprite double face for the supplied side of a normal cube
     * @param quads List of quads to put faces in
     * @param facing Side of block to generate side for
     * @param condition Condition ID for sides
     */
    public static void putFace(List<SpritelessQuad> quads, EnumFacing facing, int condition) {

        switch(facing) {
        case DOWN:
            fullSpriteDoubleQuad(quads, EnumFacing.DOWN, condition,
                new Vec3(0, 0, 0),
                new Vec3(1, 0, 0),
                new Vec3(1, 0, 1),
                new Vec3(0, 0, 1)
            );
            break;
        case UP:
            fullSpriteDoubleQuad(quads, EnumFacing.UP, condition,
                new Vec3(0, 1, 0),
                new Vec3(1, 1, 0),
                new Vec3(1, 1, 1),
                new Vec3(0, 1, 1)
            );
            break;
        case NORTH:
            fullSpriteDoubleQuad(quads, EnumFacing.NORTH, condition,
                new Vec3(0, 1, 0),
                new Vec3(1, 1, 0),
                new Vec3(1, 0, 0),
                new Vec3(0, 0, 0)
            );
            break;
        case SOUTH:
            fullSpriteDoubleQuad(quads, EnumFacing.SOUTH, condition,
                new Vec3(1, 1, 1),
                new Vec3(0, 1, 1),
                new Vec3(0, 0, 1),
                new Vec3(1, 0, 1)
            );
            break;
        case EAST:
            fullSpriteDoubleQuad(quads, EnumFacing.EAST, condition,
                new Vec3(1, 1, 0),
                new Vec3(1, 1, 1),
                new Vec3(1, 0, 1),
                new Vec3(1, 0, 0)
            );
            break;
        case WEST:
            fullSpriteDoubleQuad(quads, EnumFacing.WEST, condition,
                new Vec3(0, 1, 1),
                new Vec3(0, 1, 0),
                new Vec3(0, 0, 0),
                new Vec3(0, 0, 1)
            );
            break;
        }
    }


    public static int[] vertexToInts(double x, double y, double z, float u, float v, TextureAtlasSprite sprite) {
        return new int[] {
            Float.floatToRawIntBits((float) x),
            Float.floatToRawIntBits((float) y),
            Float.floatToRawIntBits((float) z),
            -1,
            Float.floatToRawIntBits(sprite.getInterpolatedU(u)),
            Float.floatToRawIntBits(sprite.getInterpolatedV(v)),
            0
        };
    }
}

ModelHandler

Letting Minecraft know exactly what you want where

[Error: put your package here]

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Supplier;

import catwalks.Const;
import net.minecraft.block.Block;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.renderer.block.statemap.StateMapperBase;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.resources.model.IBakedModel;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.client.event.ModelBakeEvent;
import net.minecraftforge.client.event.TextureStitchEvent;
import net.minecraftforge.client.model.ModelLoader;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;

public class ModelHandler {
    public static String MODID = [Error: reference modid here];
    public static final ModelHandler INSTANCE = new ModelHandler();
    
    public ModelHandler() {
        MinecraftForge.EVENT_BUS.register(this);
    }
    
    public void registerTexture(String path) {
        textures.add(new ResourceLocation(MODID + ":" + path));
    }
    
    public void registerModel(String name, Supplier<IBakedModel> generator) {
        registeredModels.put(name, generator);
    }
    
    public static void setStaticMap(Block block, String loc) {
        ModelLoader.setCustomStateMapper(block, new StateMapperStatic(loc));
    }
    
    // boring implementation details below
    
    private Map<String, Supplier<IBakedModel>> registeredModels = new HashMap<>();
    private Map<ModelResourceLocation, IBakedModel> modelsToInsert = new HashMap<>();
    
    {/* models */}
    
    private void model(String loc, IBakedModel model) {
        modelsToInsert.put(new ModelResourceLocation(MODID + ":" + loc), model);
    }
    
    @SubscribeEvent
    public void onModelBakeEvent(ModelBakeEvent event) {
        modelsToInsert.clear();
        
        for(Entry<String, Supplier<IBakedModel>> entry : registeredModels.entrySet()) {
            model(entry.getKey(), entry.getValue().get());
        }
        
        for (Entry<ModelResourceLocation, IBakedModel> model : modelsToInsert.entrySet()) {
            event.modelRegistry.putObject(model.getKey(), model.getValue());
        }
    }
    
    {/* textures */}
    
    public List<ResourceLocation> textures = new ArrayList<>();
    
    @SubscribeEvent
    public void textureStitch(TextureStitchEvent.Pre event) {
        
        TextureMap map = event.map;
        
        for(ResourceLocation tex : textures) {
            
            map.getTextureExtry(tex.toString());
            TextureAtlasSprite texture = map.getTextureExtry(tex.toString());
            
            if(texture == null) {
                map.registerSprite(tex);
            }
        }

    }
    
    public static class StateMapperStatic extends StateMapperBase {

        ModelResourceLocation loc;
        
        public StateMapperStatic(String loc) {
            this.loc = new ModelResourceLocation(MODID + ":" + loc);
        }
        
        @Override
        protected ModelResourceLocation getModelResourceLocation(IBlockState state) {
            return loc;
        }
        
    }
}