src/model/index.js
import * as THREE from "three";
require("three-instanced-mesh")(THREE);
import * as $ from 'jquery';
import merge from 'deepmerge'
import Render, { defaultOptions, deepDisposeMesh, mergeCubeMeshes } from "../renderBase";
import { loadTextureAsBase64, scaleUv, DEFAULT_ROOT, loadJsonFromPath, loadBlockState, loadTextureMeta } from "../functions";
import ModelConverter from "./modelConverter";
import * as md5 from "md5";
import { parseModel, loadAndMergeModel, loadModelTexture, modelCacheKey, toRadians, deleteObjectProperties, loadTextures } from "./modelFunctions";
import work from 'webworkify-webpack';
import SkinRender from "../skin";
const ModelWorker = require.resolve("./ModelWorker.js");
String.prototype.replaceAll = function (search, replacement) {
let target = this;
return target.replace(new RegExp(search, 'g'), replacement);
};
const colors = [
0xFF0000,
0x00FFFF,
0x0000FF,
0x000080,
0xFF00FF,
0x800080,
0x808000,
0x00FF00,
0x008000,
0xFFFF00,
0x800000,
0x008080,
];
const FACE_ORDER = ["east", "west", "up", "down", "south", "north"];
const TINTS = ["lightgreen"];
const mergedModelCache = {};
const loadedTextureCache = {};
const modelInstances = {};
const textureCache = {};
const canvasCache = {};
const materialCache = {};
const geometryCache = {};
const instanceCache = {};
const animatedTextures = [];
/**
* @see defaultOptions
* @property {string} type alternative way to specify the model type (block/item)
* @property {boolean} [centerCubes=false] center the cube's rotation point
* @property {string} [assetRoot=DEFAULT_ROOT] root to get asset files from
*/
let defOptions = {
camera: {
type: "perspective",
x: 35,
y: 25,
z: 20,
target: [0, 0, 0]
},
type: "block",
centerCubes: false,
assetRoot: DEFAULT_ROOT,
useWebWorkers: false
};
/**
* A renderer for Minecraft models, i.e. blocks & items
*/
class ModelRender extends Render {
/**
* @param {Object} [options] The options for this renderer, see {@link defaultOptions}
* @param {string} [options.assetRoot=DEFAULT_ROOT] root to get asset files from
*
* @param {HTMLElement} [element=document.body] DOM Element to attach the renderer to - defaults to document.body
* @constructor
*/
constructor(options, element) {
super(options, defOptions, element);
this.renderType = "ModelRender";
this.models = [];
this.attached = false;
}
/**
* Does the actual rendering
* @param {(string[]|Object[])} models Array of models to render - Either strings in the format <block|item>/<model name> or objects
* @param {string} [models[].type=block] either 'block' or 'item'
* @param {string} models[].model if 'type' is given, just the block/item name otherwise '<block|item>/<model name>'
* @param {number[]} [models[].offset] [x,y,z] array of the offset
* @param {number[]} [models[].rotation] [x,y,z] array of the rotation
* @param {string} [models[].blockstate] name of a blockstate to be used to determine the models (only for blocks)
* @param {string} [models[].variant=normal] if 'blockstate' is given, the block variant to use
* @param {function} [cb] Callback when rendering finished
*/
render(models, cb) {
let modelRender = this;
if (!modelRender.attached && !modelRender._scene) {// Don't init scene if attached, since we already have an available scene
super.initScene(function () {
// Animate textures
for (let i = 0; i < animatedTextures.length; i++) {
animatedTextures[i]();
}
modelRender.element.dispatchEvent(new CustomEvent("modelRender", {detail: {models: modelRender.models}}));
});
} else {
console.log("[ModelRender] is attached - skipping scene init");
}
let parsedModelList = [];
parseModels(modelRender, models, parsedModelList)
.then(() => loadAndMergeModels(modelRender, parsedModelList))
.then(() => loadModelTextures(modelRender, parsedModelList))
.then(() => doModelRender(modelRender, parsedModelList))
.then((renderedModels) => {
console.timeEnd("doModelRender");
console.debug(renderedModels)
if (typeof cb === "function") cb();
})
}
}
function parseModels(modelRender, models, parsedModelList) {
console.time("parseModels");
console.log("Parsing Models...");
let parsePromises = [];
for (let i = 0; i < models.length; i++) {
let model = models[i];
// parsePromises.push(parseModel(model, model, parsedModelList, modelRender.options.assetRoot))
parsePromises.push(new Promise(resolve => {
if(modelRender.options.useWebWorkers) {
let w = work(ModelWorker);
w.addEventListener('message', event => {
parsedModelList.push(...event.data.parsedModelList);
resolve();
});
w.postMessage({
func: "parseModel",
model: model,
modelOptions: model,
parsedModelList: parsedModelList,
assetRoot: modelRender.options.assetRoot
})
}else{
parseModel(model, model, parsedModelList, modelRender.options.assetRoot).then(()=>{
resolve();
})
}
}))
}
return Promise.all(parsePromises);
}
function loadAndMergeModels(modelRender, parsedModelList) {
console.timeEnd("parseModels");
console.time("loadAndMergeModels");
let jsonPromises = [];
console.log("Loading Model JSON data & merging...");
let uniqueModels = {};
for (let i = 0; i < parsedModelList.length; i++) {
let cacheKey = modelCacheKey(parsedModelList[i]);
modelInstances[cacheKey] = (modelInstances[cacheKey] || 0) + 1;
uniqueModels[cacheKey] = parsedModelList[i];
}
let uniqueModelList = Object.values(uniqueModels);
console.debug(uniqueModelList.length + " unique models");
for (let i = 0; i < uniqueModelList.length; i++) {
jsonPromises.push(new Promise(resolve => {
let model = uniqueModelList[i];
let cacheKey = modelCacheKey(model);
console.debug("loadAndMerge " + cacheKey);
if (mergedModelCache.hasOwnProperty(cacheKey)) {
resolve();
return;
}
if(modelRender.options.useWebWorkers) {
let w = work(ModelWorker);
w.addEventListener('message', event => {
mergedModelCache[cacheKey] = event.data.mergedModel;
resolve();
});
w.postMessage({
func: "loadAndMergeModel",
model: model,
assetRoot: modelRender.options.assetRoot
});
}else{
loadAndMergeModel(model,modelRender.options.assetRoot).then((mergedModel)=>{
mergedModelCache[cacheKey] = mergedModel;
resolve();
})
}
}))
}
return Promise.all(jsonPromises);
}
function loadModelTextures(modelRender, parsedModelList) {
console.timeEnd("loadAndMergeModels");
console.time("loadModelTextures");
let texturePromises = [];
console.log("Loading Textures...");
let uniqueModels = {};
for (let i = 0; i < parsedModelList.length; i++) {
uniqueModels[modelCacheKey(parsedModelList[i])] = parsedModelList[i];
}
let uniqueModelList = Object.values(uniqueModels);
console.debug(uniqueModelList.length + " unique models");
for (let i = 0; i < uniqueModelList.length; i++) {
texturePromises.push(new Promise(resolve => {
let model = uniqueModelList[i];
let cacheKey = modelCacheKey(model);
console.debug("loadTexture " + cacheKey);
let mergedModel = mergedModelCache[cacheKey];
if (loadedTextureCache.hasOwnProperty(cacheKey)) {
resolve();
return;
}
if (!mergedModel) {
console.warn("Missing merged model");
console.warn(model.name);
resolve();
return;
}
if (!mergedModel.textures) {
console.warn("The model doesn't have any textures!");
console.warn("Please make sure you're using the proper file.");
console.warn(model.name);
resolve();
return;
}
if(modelRender.options.useWebWorkers) {
let w = work(ModelWorker);
w.addEventListener('message', event => {
loadedTextureCache[cacheKey] = event.data.textures;
resolve();
});
w.postMessage({
func: "loadTextures",
textures: mergedModel.textures,
assetRoot: modelRender.options.assetRoot
});
}else{
loadTextures(mergedModel.textures, modelRender.options.assetRoot).then((textures)=>{
loadedTextureCache[cacheKey] = textures;
resolve();
})
}
}))
}
return Promise.all(texturePromises);
}
function doModelRender(modelRender, parsedModelList) {
console.timeEnd("loadModelTextures");
console.time("doModelRender");
console.log("Rendering Models...");
let renderPromises = [];
for (let i = 0; i < parsedModelList.length; i++) {
renderPromises.push(new Promise(resolve => {
let model = parsedModelList[i];
let mergedModel = mergedModelCache[modelCacheKey(model)];
let textures = loadedTextureCache[modelCacheKey(model)];
let offset = model.offset || [0, 0, 0];
let rotation = model.rotation || [0, 0, 0];
let scale = model.scale || [1, 1, 1];
if (model.options.hasOwnProperty("display")) {
if (mergedModel.hasOwnProperty("display")) {
if (mergedModel.display.hasOwnProperty(model.options.display)) {
let displayData = mergedModel.display[model.options.display];
if (displayData.hasOwnProperty("translation")) {
offset = [offset[0] + displayData.translation[0], offset[1] + displayData.translation[1], offset[2] + displayData.translation[2]];
}
if (displayData.hasOwnProperty("rotation")) {
rotation = [rotation[0] + displayData.rotation[0], rotation[1] + displayData.rotation[1], rotation[2] + displayData.rotation[2]];
}
if (displayData.hasOwnProperty("scale")) {
scale = [displayData.scale[0], displayData.scale[1], displayData.scale[2]];
}
}
}
}
renderModel(modelRender, mergedModel, textures, mergedModel.textures, model.type, model.name, model.variant, offset, rotation, scale).then((renderedModel) => {
if (renderedModel.firstInstance) {
let container = new THREE.Object3D();
container.add(renderedModel.mesh);
modelRender.models.push(container);
modelRender.addToScene(container);
}
resolve(renderedModel);
})
}))
}
return Promise.all(renderPromises);
}
let renderModel = function (modelRender, model, textures, textureNames, type, name, variant, offset, rotation, scale) {
return new Promise((resolve) => {
if (model.hasOwnProperty("elements")) {// block OR item with block parent
let modelKey = modelCacheKey({type: type, name: name, variant: variant});
let instanceCount = modelInstances[modelKey];
let applyModelTransforms = function (mesh, instanceIndex) {
mesh.userData.modelType = type;
mesh.userData.modelName = name;
let _v3o = new THREE.Vector3();
let _v3s = new THREE.Vector3();
let _q = new THREE.Quaternion();
if (rotation) {
mesh.setQuaternionAt(instanceIndex, _q.setFromEuler(new THREE.Euler(toRadians(rotation[0]), toRadians(Math.abs(rotation[0]) > 0 ? rotation[1] : -rotation[1]), toRadians(rotation[2]))));
}
if (offset) {
mesh.setPositionAt(instanceIndex, _v3o.set(offset[0], offset[1], offset[2]));
}
if (scale) {
mesh.setScaleAt(instanceIndex, _v3s.set(scale[0], scale[1], scale[2]));
}
mesh.needsUpdate();
// mesh.position = _v3o;
// Object.defineProperty(mesh.position,"x",{
// get:function () {
// return this._x||0;
// },
// set:function (x) {
// this._x=x;
// mesh.setPositionAt(instanceIndex, _v3o.set(x, this.y, this.z));
// }
// });
// Object.defineProperty(mesh.position,"y",{
// get:function () {
// return this._y||0;
// },
// set:function (y) {
// this._y=y;
// mesh.setPositionAt(instanceIndex, _v3o.set(this.x, y, this.z));
// }
// });
// Object.defineProperty(mesh.position,"z",{
// get:function () {
// return this._z||0;
// },
// set:function (z) {
// this._z=z;
// mesh.setPositionAt(instanceIndex, _v3o.set(this.x, this.y, z));
// }
// })
//
// mesh.rotation = new THREE.Euler(toRadians(rotation[0]), toRadians(Math.abs(rotation[0]) > 0 ? rotation[1] : -rotation[1]), toRadians(rotation[2]));
// Object.defineProperty(mesh.rotation,"x",{
// get:function () {
// return this._x||0;
// },
// set:function (x) {
// this._x=x;
// mesh.setQuaternionAt(instanceIndex, _q.setFromEuler(new THREE.Euler(toRadians(x), toRadians(this.y), toRadians(this.z))));
// }
// });
// Object.defineProperty(mesh.rotation,"y",{
// get:function () {
// return this._y||0;
// },
// set:function (y) {
// this._y=y;
// mesh.setQuaternionAt(instanceIndex, _q.setFromEuler(new THREE.Euler(toRadians(this.x), toRadians(y), toRadians(this.z))));
// }
// });
// Object.defineProperty(mesh.rotation,"z",{
// get:function () {
// return this._z||0;
// },
// set:function (z) {
// this._z=z;
// mesh.setQuaternionAt(instanceIndex, _q.setFromEuler(new THREE.Euler(toRadians(this.x), toRadians(this.y), toRadians(z))));
// }
// });
//
// mesh.scale = _v3s;
resolve({
mesh: mesh,
firstInstance: instanceIndex === 0
});
};
let finalizeCubeModel = function (geometry, materials) {
geometry.translate(-8, -8, -8);
let cachedInstance;
if (!instanceCache.hasOwnProperty(modelKey)) {
console.debug("Caching new model instance " + modelKey + " (with " + instanceCount + " instances)");
let newInstance = new THREE.InstancedMesh(
geometry,
materials,
instanceCount,
false,
false,
false);
cachedInstance = {
instance: newInstance,
index: 0
};
instanceCache[modelKey] = cachedInstance;
let _v3o = new THREE.Vector3();
let _v3s = new THREE.Vector3(1, 1, 1);
let _q = new THREE.Quaternion();
for (let i = 0; i < instanceCount; i++) {
newInstance.setQuaternionAt(i, _q);
newInstance.setPositionAt(i, _v3o);
newInstance.setScaleAt(i, _v3s);
}
} else {
console.debug("Using cached instance (" + modelKey + ")");
cachedInstance = instanceCache[modelKey];
}
applyModelTransforms(cachedInstance.instance, cachedInstance.index++);
};
if (instanceCache.hasOwnProperty(modelKey)) {
console.debug("Using cached model instance (" + modelKey + ")");
let cachedInstance = instanceCache[modelKey];
applyModelTransforms(cachedInstance.instance, cachedInstance.index++);
return;
}
// Render the elements
let promises = [];
for (let i = 0; i < model.elements.length; i++) {
let element = model.elements[i];
// // From net.minecraft.client.renderer.block.model.BlockPart.java#47 - https://yeleha.co/2JcqSr4
let fallbackFaces = {
down: {
uv: [element.from[0], 16 - element.to[2], element.to[0], 16 - element.from[2]],
texture: "#down"
},
up: {
uv: [element.from[0], element.from[2], element.to[0], element.to[2]],
texture: "#up"
},
north: {
uv: [16 - element.to[0], 16 - element.to[1], 16 - element.from[0], 16 - element.from[1]],
texture: "#north"
},
south: {
uv: [element.from[0], 16 - element.to[1], element.to[0], 16 - element.from[1]],
texture: "#south"
},
west: {
uv: [element.from[2], 16 - element.to[1], element.to[2], 16 - element.from[2]],
texture: "#west"
},
east: {
uv: [16 - element.to[2], 16 - element.to[1], 16 - element.from[2], 16 - element.from[1]],
texture: "#east"
}
};
promises.push(new Promise((resolve) => {
let baseName =name.replaceAll(" ", "_").replaceAll("-", "_").toLowerCase() + "_" + (element.__comment ? element.__comment.replaceAll(" ", "_").replaceAll("-", "_").toLowerCase() + "_" : "");
createCube(element.to[0] - element.from[0], element.to[1] - element.from[1], element.to[2] - element.from[2],
baseName + Date.now(),
element.faces, fallbackFaces, textures, textureNames, modelRender.options.assetRoot, baseName)
.then((cube) => {
cube.applyMatrix(new THREE.Matrix4().makeTranslation((element.to[0] - element.from[0]) / 2, (element.to[1] - element.from[1]) / 2, (element.to[2] - element.from[2]) / 2));
cube.applyMatrix(new THREE.Matrix4().makeTranslation(element.from[0], element.from[1], element.from[2]));
if (element.rotation) {
rotateAboutPoint(cube,
new THREE.Vector3(element.rotation.origin[0], element.rotation.origin[1], element.rotation.origin[2]),
new THREE.Vector3(element.rotation.axis === "x" ? 1 : 0, element.rotation.axis === "y" ? 1 : 0, element.rotation.axis === "z" ? 1 : 0),
toRadians(element.rotation.angle));
}
resolve(cube);
})
}));
}
Promise.all(promises).then((cubes) => {
let mergedCubes = mergeCubeMeshes(cubes, true);
mergedCubes.sourceSize = cubes.length;
finalizeCubeModel(mergedCubes.geometry, mergedCubes.materials, cubes.length);
for (let i = 0; i < cubes.length; i++) {
deepDisposeMesh(cubes[i], true);
}
})
} else {// 2d item
createPlane(name + "_" + Date.now(), textures).then((plane) => {
if (offset) {
plane.applyMatrix(new THREE.Matrix4().makeTranslation(offset[0], offset[1], offset[2]))
}
if (rotation) {
plane.rotation.set(toRadians(rotation[0]), toRadians(Math.abs(rotation[0]) > 0 ? rotation[1] : -rotation[1]), toRadians(rotation[2]));
}
if (scale) {
plane.scale.set(scale[0], scale[1], scale[2]);
}
resolve({
mesh: plane,
firstInstance: true
});
})
}
})
};
let createDot = function (c) {
let dotGeometry = new THREE.Geometry();
dotGeometry.vertices.push(new THREE.Vector3());
let dotMaterial = new THREE.PointsMaterial({size: 5, sizeAttenuation: false, color: c});
return new THREE.Points(dotGeometry, dotMaterial);
};
let createPlane = function (name, textures) {
return new Promise((resolve) => {
let materialLoaded = function (material, width, height) {
let geometry = new THREE.PlaneGeometry(width, height);
let plane = new THREE.Mesh(geometry, material);
plane.name = name;
plane.receiveShadow = true;
resolve(plane);
};
if (textures) {
let w = 0, h = 0;
let promises = [];
for (let t in textures) {
if (textures.hasOwnProperty(t)) {
promises.push(new Promise((resolve) => {
let img = new Image();
img.onload = function () {
if (img.width > w) w = img.width;
if (img.height > h) h = img.height;
resolve(img);
};
img.src = textures[t];
}))
}
}
Promise.all(promises).then((images) => {
let canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
let context = canvas.getContext("2d");
for (let i = 0; i < images.length; i++) {
let img = images[i];
context.drawImage(img, 0, 0);
}
let data = canvas.toDataURL("image/png");
let hash = md5(data);
if (materialCache.hasOwnProperty(hash)) {// Use material from cache
console.debug("Using cached Material (" + hash + ")");
materialLoaded(materialCache[hash], w, h);
return;
}
let textureLoaded = function (texture) {
let material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
side: THREE.DoubleSide,
alphaTest: 0.5,
name: name
});
// Add material to cache
console.debug("Caching Material " + hash);
materialCache[hash] = material;
materialLoaded(material, w, h);
};
if (textureCache.hasOwnProperty(hash)) {// Use texture to cache
console.debug("Using cached Texture (" + hash + ")");
textureLoaded(textureCache[hash]);
return;
}
console.debug("Pre-Caching Texture " + hash);
textureCache[hash] = new THREE.TextureLoader().load(data, function (texture) {
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.anisotropy = 0;
texture.needsUpdate = true;
console.debug("Caching Texture " + hash);
// Add texture to cache
textureCache[hash] = texture;
textureLoaded(texture);
});
});
}
})
};
/// From https://github.com/InventivetalentDev/SkinRender/blob/master/js/render/skin.js#L353
let createCube = function (width, height, depth, name, faces, fallbackFaces, textures, textureNames, assetRoot, baseName) {
return new Promise((resolve) => {
let geometryKey = width + "_" + height + "_" + depth;
let geometry;
if (geometryCache.hasOwnProperty(geometryKey)) {
console.debug("Using cached Geometry (" + geometryKey + ")");
geometry = geometryCache[geometryKey];
} else {
geometry = new THREE.BoxGeometry(width, height, depth);
console.debug("Caching Geometry " + geometryKey);
geometryCache[geometryKey] = geometry;
}
let materialsLoaded = function (materials) {
let cube = new THREE.Mesh(geometry, materials);
cube.name = name;
cube.receiveShadow = true;
resolve(cube);
};
if (textures) {
let promises = [];
for (let i = 0; i < 6; i++) {
promises.push(new Promise((resolve) => {
let f = FACE_ORDER[i];
if (!faces.hasOwnProperty(f)) {
// console.warn("Missing face: " + f + " in model " + name);
resolve(null);
return;
}
let face = faces[f];
let textureRef = face.texture.substr(1);
if (!textures.hasOwnProperty(textureRef)) {
console.warn("Missing texture '" + textureRef + "' for face " + f + " in model " + name);
resolve(null);
return;
}
let canvasKey = textureRef + "_" + f + "_" + baseName;
let processImgToCanvasData = (img)=>{
let uv = face.uv;
if (!uv) {
// console.warn("Missing UV mapping for face " + f + " in model " + name + ". Using defaults");
uv = fallbackFaces[f].uv;
}
// Scale the uv values to match the image width, so we can support resource packs with higher-resolution textures
uv = [
scaleUv(uv[0], img.width),
scaleUv(uv[1], img.height),
scaleUv(uv[2], img.width),
scaleUv(uv[3], img.height)
];
let canvas = document.createElement("canvas");
canvas.width = Math.abs(uv[2] - uv[0]);
canvas.height = Math.abs(uv[3] - uv[1]);
let context = canvas.getContext("2d");
context.drawImage(img, Math.min(uv[0], uv[2]), Math.min(uv[1], uv[3]), canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
if (face.hasOwnProperty("tintindex")) {
context.fillStyle = TINTS[face.tintindex];
context.globalCompositeOperation = 'multiply';
context.fillRect(0, 0, canvas.width, canvas.height);
context.globalAlpha = 1;
context.globalCompositeOperation = 'destination-in';
context.drawImage(img, Math.min(uv[0], uv[2]), Math.min(uv[1], uv[3]), canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
// context.globalAlpha = 0.5;
// context.beginPath();
// context.fillStyle = "green";
// context.rect(0, 0, uv[2] - uv[0], uv[3] - uv[1]);
// context.fill();
// context.globalAlpha = 1.0;
}
let canvasData = context.getImageData(0, 0, canvas.width, canvas.height).data;
let hasTransparency = false;
for (let i = 3; i < (canvas.width * canvas.height); i += 4) {
if (canvasData[i] < 255) {
hasTransparency = true;
break;
}
}
let dataUrl = canvas.toDataURL("image/png");
let dataHash = md5(dataUrl);
let d = {
data: canvasData,
dataUrl: dataUrl,
dataUrlHash: dataHash,
hasTransparency: hasTransparency,
width: canvas.width,
height: canvas.height
};
console.debug("Caching new canvas ("+canvasKey+"/"+dataHash+")")
canvasCache[canvasKey] = d;
return d;
};
let loadTextureFromCanvas = (canvas)=>{
let loadTextureDefault = function (canvas) {
let data = canvas.dataUrl;
let hash =canvas.dataUrlHash;
let hasTransparency = canvas.hasTransparency;
if (materialCache.hasOwnProperty(hash)) {// Use material from cache
console.debug("Using cached Material (" + hash + ")");
resolve(materialCache[hash]);
return;
}
let textureLoaded = function (texture) {
let n = textureNames[textureRef];
if (n.startsWith("#")) {
n = textureNames[name.substr(1)];
}
let material = new THREE.MeshBasicMaterial({
map: texture,
transparent: hasTransparency,
side: hasTransparency ? THREE.DoubleSide : THREE.FrontSide,
alphaTest: 0.5,
name: f + "_" + textureRef + "_" + n
});
// Add material to cache
console.debug("Caching Material " + hash);
materialCache[hash] = material;
resolve(material);
};
if (textureCache.hasOwnProperty(hash)) {// Use texture from cache
console.debug("Using cached Texture (" + hash + ")");
textureLoaded(textureCache[hash]);
return;
}
console.debug("Pre-Caching Texture " + hash);
textureCache[hash] = new THREE.TextureLoader().load(data, function (texture) {
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.anisotropy = 0;
texture.needsUpdate = true;
if (face.hasOwnProperty("rotation")) {
texture.center.x = .5;
texture.center.y = .5;
texture.rotation = toRadians(face.rotation);
}
console.debug("Caching Texture " + hash);
// Add texture to cache
textureCache[hash] = texture;
textureLoaded(texture);
});
};
let loadTextureWithMeta = function (canvas, meta) {
let hasTransparency = canvas.hasTransparency;
let frametime = 1;
if (meta.hasOwnProperty("animation")) {
if (meta.animation.hasOwnProperty("frametime")) {
frametime = meta.animation.frametime;
}
}
let parts = Math.floor(canvas.height / canvas.width);
let promises1 = [];
for (let i = 0; i < parts; i++) {
promises1.push(new Promise((resolve) => {
let canvas1 = document.createElement("canvas");
canvas1.width = canvas.width;
canvas1.height = canvas.width;
let context1 = canvas1.getContext("2d");
context1.drawImage(canvas, 0, i * canvas.width, canvas.width, canvas.width, 0, 0, canvas.width, canvas.width);
let data = canvas1.toDataURL("image/png");
let hash = md5(data);
if (textureCache.hasOwnProperty(hash)) {// Use texture to cache
console.debug("Using cached Texture (" + hash + ")");
resolve(textureCache[hash]);
return;
}
console.debug("Pre-Caching Texture " + hash);
textureCache[hash] = new THREE.TextureLoader().load(data, function (texture) {
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.anisotropy = 0;
texture.needsUpdate = true;
console.debug("Caching Texture " + hash);
// add texture to cache
textureCache[hash] = texture;
resolve(texture);
});
}));
}
Promise.all(promises1).then((textures) => {
// Don't cache this material, since it's animated
let material = new THREE.MeshBasicMaterial({
map: textures[0],
transparent: hasTransparency,
side: hasTransparency ? THREE.DoubleSide : THREE.FrontSide,
alphaTest: 0.5
});
let frameCounter = 0;
let textureIndex = 0;
animatedTextures.push(() => {// called on render
if (frameCounter >= frametime) {
frameCounter = 0;
// Set new texture
material.map = textures[textureIndex];
textureIndex++;
}
if (textureIndex >= textures.length) {
textureIndex = 0;
}
frameCounter += 0.1;// game ticks TODO: figure out the proper value for this
})
resolve(material);
});
};
if ((canvas.height > canvas.width) && (canvas.height % canvas.width === 0)) {// Taking a guess that this is an animated texture
let name = textureNames[textureRef];
if (name.startsWith("#")) {
name = textureNames[name.substr(1)];
}
if (name.indexOf("/") !== -1) {
name = name.substr(name.indexOf("/") + 1);
}
loadTextureMeta(name, assetRoot).then((meta) => {
loadTextureWithMeta(canvas, meta);
}).catch(() => {// Guessed wrong :shrug:
loadTextureDefault(canvas);
})
} else {
loadTextureDefault(canvas);
}
};
if (canvasCache.hasOwnProperty(canvasKey)) {
let cachedCanvas = canvasCache[canvasKey];
if (cachedCanvas.hasOwnProperty("img")) {
console.debug("Waiting for canvas image that's already loading ("+canvasKey+")")
let img= cachedCanvas.img;
img.waitingForCanvas.push(function (canvas) {
loadTextureFromCanvas(canvas);
});
} else {
console.debug("Using cached canvas (" + canvasKey + ")")
loadTextureFromCanvas(canvasCache[canvasKey]);
}
} else {
let img = new Image();
img.onerror = function (err) {
console.warn(err);
resolve(null);
};
img.waitingForCanvas = [];
img.onload = function () {
let canvasData = processImgToCanvasData(img);
loadTextureFromCanvas(canvasData);
for (let c = 0; c < img.waitingForCanvas.length; c++) {
img.waitingForCanvas[c](canvasData);
}
};
console.debug("Pre-caching canvas (" + canvasKey + ")");
canvasCache[canvasKey] = {
img: img
};
img.src = textures[textureRef];
}
}));
}
Promise.all(promises).then(materials => materialsLoaded(materials))
} else {
let materials = [];
for (let i = 0; i < 6; i++) {
materials.push(new THREE.MeshBasicMaterial({
color: colors[i + 2],
wireframe: true
}))
}
materialsLoaded(materials);
}
// if (textures) {
// applyCubeTextureToGeometry(geometry, texture, uv, width, height, depth);
// }
})
};
/// https://stackoverflow.com/questions/42812861/three-js-pivot-point/42866733#42866733
// obj - your object (THREE.Object3D or derived)
// point - the point of rotation (THREE.Vector3)
// axis - the axis of rotation (normalized THREE.Vector3)
// theta - radian value of rotation
function rotateAboutPoint(obj, point, axis, theta) {
obj.position.sub(point); // remove the offset
obj.position.applyAxisAngle(axis, theta); // rotate the POSITION
obj.position.add(point); // re-add the offset
obj.rotateOnAxis(axis, theta); // rotate the OBJECT
}
ModelRender.cache = {
loadedTextures: loadedTextureCache,
mergedModels: mergedModelCache,
instanceCount: modelInstances,
texture: textureCache,
canvas: canvasCache,
material: materialCache,
geometry: geometryCache,
instances: instanceCache,
resetInstances: function () {
deleteObjectProperties(modelInstances);
deleteObjectProperties(instanceCache);
},
clearAll: function () {
deleteObjectProperties(loadedTextureCache);
deleteObjectProperties(mergedModelCache);
deleteObjectProperties(modelInstances);
deleteObjectProperties(textureCache);
deleteObjectProperties(materialCache);
deleteObjectProperties(geometryCache);
deleteObjectProperties(instanceCache);
}
};
ModelRender.ModelConverter = ModelConverter;
if (typeof window !== "undefined") {
window.ModelRender = ModelRender;
window.ModelConverter = ModelConverter;
}
if (typeof global !== "undefined")
global.ModelRender = ModelRender;
export default ModelRender;