Key parts are :
ICustomModelLoader -> Provide a dummy IModel here pointing to your actual IBakedModel.
IExtendedBlockState -> Override Block::getExtendedState & Block::createBlockState to provide IUnlistedProperties. These are used to provide data to the IBakedModel through the IBlockState
List<BakedQuad> -> This is the model boiled down. IBakedModel::getQuads is where you'll be doing your logic, per block-face. Be sure to as said cache your models, aka put the List<BakedQuad> somewhere. Always check if you have the wanted model, and only if you do not, should you make one from scratch. This is the optimization over TESR's.
I am doing this in my project Echo (for 1.10.2, but procedure should be the same for 1.12.2) to allow any block (well, full blocks) to be compressed into Menger-fractals. ModelLoader, IBakedModel, Block