Created
October 22, 2022 12:35
-
-
Save mraliscoder/88265482b11c566c0641940f9de645d9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const TAU = 2 * Math.PI; | |
const EPSILON = 1e-3; | |
const skinLayout = [ | |
{ | |
head: [ | |
{ | |
l: { x: 16, y: 8, w: 8, h: 8 }, | |
r: { x: 0, y: 8, w: 8, h: 8 }, | |
u: { x: 8, y: 0, w: 8, h: 8 }, | |
d: { x: 16, y: 7, w: 8, h: -8 }, | |
f: { x: 8, y: 8, w: 8, h: 8 }, | |
b: { x: 24, y: 8, w: 8, h: 8 }, | |
}, | |
{ | |
l: { x: 48, y: 8, w: 8, h: 8 }, | |
r: { x: 32, y: 8, w: 8, h: 8 }, | |
u: { x: 40, y: 0, w: 8, h: 8 }, | |
d: { x: 48, y: 7, w: 8, h: -8 }, | |
f: { x: 40, y: 8, w: 8, h: 8 }, | |
b: { x: 56, y: 8, w: 8, h: 8 }, | |
}, | |
], | |
torso: [ | |
{ | |
l: { x: 28, y: 20, w: 4, h: 12 }, | |
r: { x: 16, y: 20, w: 4, h: 12 }, | |
u: { x: 20, y: 16, w: 8, h: 4 }, | |
d: { x: 28, y: 19, w: 8, h: -4 }, | |
f: { x: 20, y: 20, w: 8, h: 12 }, | |
b: { x: 32, y: 20, w: 8, h: 12 }, | |
}, | |
], | |
armR: [ | |
{ | |
l: { x: 48, y: 20, w: 4, h: 12 }, | |
r: { x: 40, y: 20, w: 4, h: 12 }, | |
u: { x: 44, y: 16, w: 4, h: 4 }, | |
d: { x: 48, y: 19, w: 4, h: -4 }, | |
f: { x: 44, y: 20, w: 4, h: 12 }, | |
b: { x: 52, y: 20, w: 4, h: 12 }, | |
}, | |
], | |
armRS: [ | |
{ | |
l: { x: 47, y: 20, w: 4, h: 12 }, | |
r: { x: 40, y: 20, w: 4, h: 12 }, | |
u: { x: 44, y: 16, w: 3, h: 4 }, | |
d: { x: 47, y: 19, w: 3, h: -4 }, | |
f: { x: 44, y: 20, w: 3, h: 12 }, | |
b: { x: 51, y: 20, w: 3, h: 12 }, | |
}, | |
], | |
armL: [ | |
{ | |
l: { x: 43, y: 20, w: -4, h: 12 }, | |
r: { x: 51, y: 20, w: -4, h: 12 }, | |
u: { x: 47, y: 16, w: -4, h: 4 }, | |
d: { x: 51, y: 19, w: -4, h: -4 }, | |
f: { x: 47, y: 20, w: -4, h: 12 }, | |
b: { x: 55, y: 20, w: -4, h: 12 }, | |
}, | |
], | |
armLS: [ | |
{ | |
l: { x: 43, y: 20, w: -4, h: 12 }, | |
r: { x: 50, y: 20, w: -4, h: 12 }, | |
u: { x: 46, y: 16, w: -3, h: 4 }, | |
d: { x: 49, y: 19, w: -3, h: -4 }, | |
f: { x: 46, y: 20, w: -3, h: 12 }, | |
b: { x: 53, y: 20, w: -3, h: 12 }, | |
}, | |
], | |
legR: [ | |
{ | |
l: { x: 8, y: 20, w: 4, h: 12 }, | |
r: { x: 0, y: 20, w: 4, h: 12 }, | |
u: { x: 4, y: 16, w: 4, h: 4 }, | |
d: { x: 8, y: 19, w: 4, h: -4 }, | |
f: { x: 4, y: 20, w: 4, h: 12 }, | |
b: { x: 12, y: 20, w: 4, h: 12 }, | |
}, | |
], | |
legL: [ | |
{ | |
l: { x: 3, y: 20, w: -4, h: 12 }, | |
r: { x: 11, y: 20, w: -4, h: 12 }, | |
u: { x: 7, y: 16, w: -4, h: 4 }, | |
d: { x: 11, y: 19, w: -4, h: -4 }, | |
f: { x: 7, y: 20, w: -4, h: 12 }, | |
b: { x: 15, y: 20, w: -4, h: 12 }, | |
}, | |
], | |
}, | |
{ | |
head: [ | |
{ | |
l: { x: 16, y: 8, w: 8, h: 8 }, | |
r: { x: 0, y: 8, w: 8, h: 8 }, | |
u: { x: 8, y: 0, w: 8, h: 8 }, | |
d: { x: 16, y: 7, w: 8, h: -8 }, | |
f: { x: 8, y: 8, w: 8, h: 8 }, | |
b: { x: 24, y: 8, w: 8, h: 8 }, | |
}, | |
{ | |
l: { x: 48, y: 8, w: 8, h: 8 }, | |
r: { x: 32, y: 8, w: 8, h: 8 }, | |
u: { x: 40, y: 0, w: 8, h: 8 }, | |
d: { x: 48, y: 7, w: 8, h: -8 }, | |
f: { x: 40, y: 8, w: 8, h: 8 }, | |
b: { x: 56, y: 8, w: 8, h: 8 }, | |
}, | |
], | |
torso: [ | |
{ | |
l: { x: 28, y: 20, w: 4, h: 12 }, | |
r: { x: 16, y: 20, w: 4, h: 12 }, | |
u: { x: 20, y: 16, w: 8, h: 4 }, | |
d: { x: 28, y: 19, w: 8, h: -4 }, | |
f: { x: 20, y: 20, w: 8, h: 12 }, | |
b: { x: 32, y: 20, w: 8, h: 12 }, | |
}, | |
{ | |
l: { x: 28, y: 36, w: 4, h: 12 }, | |
r: { x: 16, y: 36, w: 4, h: 12 }, | |
u: { x: 20, y: 32, w: 8, h: 4 }, | |
d: { x: 28, y: 35, w: 8, h: -4 }, | |
f: { x: 20, y: 36, w: 8, h: 12 }, | |
b: { x: 32, y: 36, w: 8, h: 12 }, | |
}, | |
], | |
armR: [ | |
{ | |
l: { x: 48, y: 20, w: 4, h: 12 }, | |
r: { x: 40, y: 20, w: 4, h: 12 }, | |
u: { x: 44, y: 16, w: 4, h: 4 }, | |
d: { x: 48, y: 19, w: 4, h: -4 }, | |
f: { x: 44, y: 20, w: 4, h: 12 }, | |
b: { x: 52, y: 20, w: 4, h: 12 }, | |
}, | |
{ | |
l: { x: 48, y: 36, w: 4, h: 12 }, | |
r: { x: 40, y: 36, w: 4, h: 12 }, | |
u: { x: 44, y: 32, w: 4, h: 4 }, | |
d: { x: 48, y: 35, w: 4, h: -4 }, | |
f: { x: 44, y: 36, w: 4, h: 12 }, | |
b: { x: 52, y: 36, w: 4, h: 12 }, | |
}, | |
], | |
armRS: [ | |
{ | |
l: { x: 47, y: 20, w: 4, h: 12 }, | |
r: { x: 40, y: 20, w: 4, h: 12 }, | |
u: { x: 44, y: 16, w: 3, h: 4 }, | |
d: { x: 47, y: 19, w: 3, h: -4 }, | |
f: { x: 44, y: 20, w: 3, h: 12 }, | |
b: { x: 51, y: 20, w: 3, h: 12 }, | |
}, | |
{ | |
l: { x: 47, y: 36, w: 4, h: 12 }, | |
r: { x: 40, y: 36, w: 4, h: 12 }, | |
u: { x: 44, y: 32, w: 3, h: 4 }, | |
d: { x: 47, y: 35, w: 3, h: -4 }, | |
f: { x: 44, y: 36, w: 3, h: 12 }, | |
b: { x: 51, y: 36, w: 3, h: 12 }, | |
}, | |
], | |
armL: [ | |
{ | |
l: { x: 40, y: 52, w: 4, h: 12 }, | |
r: { x: 32, y: 52, w: 4, h: 12 }, | |
u: { x: 36, y: 48, w: 4, h: 4 }, | |
d: { x: 40, y: 51, w: 4, h: -4 }, | |
f: { x: 36, y: 52, w: 4, h: 12 }, | |
b: { x: 44, y: 52, w: 4, h: 12 }, | |
}, | |
{ | |
l: { x: 56, y: 52, w: 4, h: 12 }, | |
r: { x: 48, y: 52, w: 4, h: 12 }, | |
u: { x: 52, y: 48, w: 4, h: 4 }, | |
d: { x: 56, y: 51, w: 4, h: -4 }, | |
f: { x: 52, y: 52, w: 4, h: 12 }, | |
b: { x: 60, y: 52, w: 4, h: 12 }, | |
}, | |
], | |
armLS: [ | |
{ | |
l: { x: 39, y: 52, w: 4, h: 12 }, | |
r: { x: 32, y: 52, w: 4, h: 12 }, | |
u: { x: 36, y: 48, w: 3, h: 4 }, | |
d: { x: 39, y: 51, w: 3, h: -4 }, | |
f: { x: 36, y: 52, w: 3, h: 12 }, | |
b: { x: 43, y: 52, w: 3, h: 12 }, | |
}, | |
{ | |
l: { x: 55, y: 52, w: 4, h: 12 }, | |
r: { x: 48, y: 52, w: 4, h: 12 }, | |
u: { x: 52, y: 48, w: 3, h: 4 }, | |
d: { x: 55, y: 51, w: 3, h: -4 }, | |
f: { x: 52, y: 52, w: 3, h: 12 }, | |
b: { x: 59, y: 52, w: 3, h: 12 }, | |
}, | |
], | |
legR: [ | |
{ | |
l: { x: 8, y: 20, w: 4, h: 12 }, | |
r: { x: 0, y: 20, w: 4, h: 12 }, | |
u: { x: 4, y: 16, w: 4, h: 4 }, | |
d: { x: 8, y: 19, w: 4, h: -4 }, | |
f: { x: 4, y: 20, w: 4, h: 12 }, | |
b: { x: 12, y: 20, w: 4, h: 12 }, | |
}, | |
{ | |
l: { x: 8, y: 36, w: 4, h: 12 }, | |
r: { x: 0, y: 36, w: 4, h: 12 }, | |
u: { x: 4, y: 32, w: 4, h: 4 }, | |
d: { x: 8, y: 35, w: 4, h: -4 }, | |
f: { x: 4, y: 36, w: 4, h: 12 }, | |
b: { x: 12, y: 36, w: 4, h: 12 }, | |
}, | |
], | |
legL: [ | |
{ | |
l: { x: 24, y: 52, w: 4, h: 12 }, | |
r: { x: 16, y: 52, w: 4, h: 12 }, | |
u: { x: 20, y: 48, w: 4, h: 4 }, | |
d: { x: 24, y: 51, w: 4, h: -4 }, | |
f: { x: 20, y: 52, w: 4, h: 12 }, | |
b: { x: 28, y: 52, w: 4, h: 12 }, | |
}, | |
{ | |
l: { x: 8, y: 52, w: 4, h: 12 }, | |
r: { x: 0, y: 52, w: 4, h: 12 }, | |
u: { x: 4, y: 48, w: 4, h: 4 }, | |
d: { x: 8, y: 51, w: 4, h: -4 }, | |
f: { x: 4, y: 52, w: 4, h: 12 }, | |
b: { x: 12, y: 52, w: 4, h: 12 }, | |
}, | |
], | |
}, | |
]; | |
function radians(d) { | |
return d * (TAU / 360); | |
} | |
function toCanvas(image, x, y, w, h) { | |
x = typeof x === "undefined" ? 0 : x; | |
y = typeof y === "undefined" ? 0 : y; | |
w = typeof w === "undefined" ? image.width : w; | |
h = typeof h === "undefined" ? image.height : h; | |
let canvas = document.createElement("canvas"); | |
canvas.width = w; | |
canvas.height = h; | |
let ctx = canvas.getContext("2d"); | |
ctx.drawImage(image, x, y, w, h, 0, 0, w, h); | |
return canvas; | |
} | |
function makeOpaque(image) { | |
let canvas = toCanvas(image); | |
let ctx = canvas.getContext("2d"); | |
let data = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
let pixels = data.data; | |
for (let p = 3; p < pixels.length; p += 4) { | |
pixels[p] = 255; | |
} | |
ctx.putImageData(data, 0, 0); | |
return canvas; | |
} | |
function hasAlphaLayer(image) { | |
let canvas = toCanvas(image); | |
let ctx = canvas.getContext("2d"); | |
let data = ctx.getImageData(0, 0, canvas.width, canvas.height); | |
let pixels = data.data; | |
for (let p = 3; p < pixels.length; p += 4) { | |
if (pixels[p] !== 255) { | |
return true; | |
} | |
} | |
return false; | |
} | |
function capeScale(height) { | |
if (height % 22 === 0) { | |
return height / 22; | |
} else if (height % 17 === 0) { | |
return height / 17; | |
} else if (height >= 32 && (height & (height - 1)) === 0) { | |
// power of 2 | |
return height / 32; | |
} else { | |
return Math.max(1, Math.floor(height / 22)); | |
} | |
} | |
function drawSkin2D() { | |
$("canvas.skin-2d").each(function (i, e) { | |
let url = e.getAttribute("data-skin-hash"); | |
let flip = e.getAttribute("data-flip") === "true"; | |
let image = new Image(); | |
image.crossOrigin = ""; | |
image.src = url; | |
image.onload = function () { | |
let opaque = makeOpaque(image); | |
let ctx = e.getContext("2d"); | |
ctx.mozImageSmoothingEnabled = false; | |
ctx.webkitImageSmoothingEnabled = false; | |
ctx.msImageSmoothingEnabled = false; | |
ctx.imageSmoothingEnabled = false; | |
if (flip) { | |
ctx.translate(e.width, e.height); | |
ctx.scale(-1, -1); | |
} | |
ctx.drawImage(opaque, 8, 8, 8, 8, 0, 0, e.width, e.height); | |
if (hasAlphaLayer(image)) { | |
ctx.drawImage(image, 40, 8, 8, 8, 0, 0, e.width, e.height); | |
} | |
}; | |
image.onerror = function () { | |
console.error("Error loading " + image.src); | |
}; | |
}); | |
$("canvas.cape-2d").each(function (i, e) { | |
let url = e.getAttribute("data-cape-hash"); | |
let flip = e.getAttribute("data-flip") === "true"; | |
let image = new Image(); | |
image.crossOrigin = ""; | |
image.src = url; | |
image.onload = function () { | |
let cs = image ? capeScale(image.height) : null; | |
console.log(image.height); | |
let opaque = makeOpaque(image); | |
let ctx = e.getContext("2d"); | |
ctx.mozImageSmoothingEnabled = false; | |
ctx.webkitImageSmoothingEnabled = false; | |
ctx.msImageSmoothingEnabled = false; | |
ctx.imageSmoothingEnabled = false; | |
if (flip) { | |
ctx.translate(e.width, e.height); | |
ctx.scale(-1, -1); | |
} | |
ctx.drawImage(opaque, cs, cs, 10 * cs, 16 * cs, 0, 0, e.width, e.height); | |
}; | |
image.onerror = function () { | |
console.error("Error loading " + image.src); | |
}; | |
}); | |
} | |
function colorFaces(geometry, canvas, rectangles) { | |
if (!rectangles) return null; | |
let pixels = canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height).data; | |
let f = 0; | |
let faces = []; | |
let materials = []; | |
let materialIndexMap = {}; | |
let side = THREE.FrontSide; | |
Object.keys(rectangles).forEach(function (k) { | |
let rect = rectangles[k]; | |
let width = Math.abs(rect.w); | |
let height = Math.abs(rect.h); | |
let dj = Math.sign(rect.w); | |
let di = Math.sign(rect.h); | |
for (let y = 0, i = rect.y; y < height; y++, i += di) { | |
for (let x = 0, j = rect.x; x < width; x++, j += dj, f += 2) { | |
let p = 4 * (i * canvas.width + j); | |
let a = pixels[p + 3]; | |
if (a === 0) { | |
side = THREE.DoubleSide; | |
continue; | |
} | |
let materialIndex = materialIndexMap[a]; | |
if (typeof materialIndex === "undefined") { | |
materials.push( | |
new THREE.MeshBasicMaterial({ | |
vertexColors: THREE.FaceColors, | |
opacity: a / 255, | |
transparent: a !== 255, | |
}) | |
); | |
materialIndex = materials.length - 1; | |
materialIndexMap[a] = materialIndex; | |
if (a !== 255) { | |
side = THREE.DoubleSide; | |
} | |
} | |
let face1 = geometry.faces[f]; | |
let face2 = geometry.faces[f + 1]; | |
face1.color.r = pixels[p] / 255; | |
face1.color.g = pixels[p + 1] / 255; | |
face1.color.b = pixels[p + 2] / 255; | |
face2.color = face1.color; | |
face1.materialIndex = materialIndex; | |
face2.materialIndex = materialIndex; | |
faces.push(face1); | |
faces.push(face2); | |
} | |
} | |
}); | |
if (faces.length === 0) return null; | |
geometry.faces = faces; | |
materials.forEach(function (m) { | |
m.side = side; | |
}); | |
return new THREE.Mesh(new THREE.BufferGeometry().fromGeometry(geometry), materials); | |
} | |
function buildMinecraftModel(skinImage, capeImage, slim, flip) { | |
if (skinImage.width < 64 || skinImage.height < 32) { | |
return null; | |
} | |
let version = skinImage.height >= 64 ? 1 : 0; | |
let cs = capeImage ? capeScale(capeImage.height) : null; | |
let opaqueSkinCanvas = makeOpaque(skinImage); | |
let transparentSkinCanvas = toCanvas(skinImage); | |
let hasAlpha = hasAlphaLayer(skinImage); | |
let headGroup = new THREE.Object3D(); | |
headGroup.position.x = 0; | |
headGroup.position.y = 12; | |
headGroup.position.z = 0; | |
let box = new THREE.BoxGeometry(8, 8, 8, 8, 8, 8); | |
let headMesh = colorFaces(box, opaqueSkinCanvas, skinLayout[version]["head"][0]); | |
headGroup.add(headMesh); | |
if (hasAlpha) { | |
box = new THREE.BoxGeometry(9, 9, 9, 8, 8, 8); | |
let hatMesh = colorFaces(box, transparentSkinCanvas, skinLayout[version]["head"][1]); | |
hatMesh && headGroup.add(hatMesh); | |
} | |
let torsoGroup = new THREE.Object3D(); | |
torsoGroup.position.x = 0; | |
torsoGroup.position.y = 2; | |
torsoGroup.position.z = 0; | |
box = new THREE.BoxGeometry(8 + EPSILON, 12 + EPSILON, 4 + EPSILON, 8, 12, 4); | |
let torsoMesh = colorFaces(box, opaqueSkinCanvas, skinLayout[version]["torso"][0]); | |
torsoGroup.add(torsoMesh); | |
if (version >= 1 && hasAlpha) { | |
box = new THREE.BoxGeometry(8.5 + EPSILON, 12.5 + EPSILON, 4.5 + EPSILON, 8, 12, 4); | |
let jacketMesh = colorFaces(box, transparentSkinCanvas, skinLayout[version]["torso"][1]); | |
jacketMesh && torsoGroup.add(jacketMesh); | |
} | |
let rightArmGroup = new THREE.Object3D(); | |
rightArmGroup.position.x = slim ? -5.5 : -6; | |
rightArmGroup.position.y = 6; | |
rightArmGroup.position.z = 0; | |
let rightArmMesh; | |
if (slim) { | |
box = new THREE.BoxGeometry(3, 12, 4, 3, 12, 4).translate(0, -4, 0); | |
rightArmMesh = colorFaces(box, opaqueSkinCanvas, skinLayout[version]["armRS"][0]); | |
} else { | |
box = new THREE.BoxGeometry(4, 12, 4, 4, 12, 4).translate(0, -4, 0); | |
rightArmMesh = colorFaces(box, opaqueSkinCanvas, skinLayout[version]["armR"][0]); | |
} | |
rightArmGroup.add(rightArmMesh); | |
if (version >= 1 && hasAlpha) { | |
let rightSleeveMesh; | |
if (slim) { | |
box = new THREE.BoxGeometry( | |
3.5 + EPSILON * 4, | |
12.5 + EPSILON * 4, | |
4.5 + EPSILON * 4, | |
3, | |
12, | |
4 | |
).translate(0, -4, 0); | |
rightSleeveMesh = colorFaces( | |
box, | |
transparentSkinCanvas, | |
skinLayout[version]["armRS"][1] | |
); | |
} else { | |
box = new THREE.BoxGeometry( | |
4.5 + EPSILON * 4, | |
12.5 + EPSILON * 4, | |
4.5 + EPSILON * 4, | |
4, | |
12, | |
4 | |
).translate(0, -4, 0); | |
rightSleeveMesh = colorFaces( | |
box, | |
transparentSkinCanvas, | |
skinLayout[version]["armR"][1] | |
); | |
} | |
rightSleeveMesh && rightArmGroup.add(rightSleeveMesh); | |
} | |
let leftArmGroup = new THREE.Object3D(); | |
leftArmGroup.position.x = slim ? 5.5 : 6; | |
leftArmGroup.position.y = 6; | |
leftArmGroup.position.z = 0; | |
let leftArmMesh; | |
if (slim) { | |
box = new THREE.BoxGeometry(3, 12, 4, 3, 12, 4).translate(0, -4, 0); | |
leftArmMesh = colorFaces(box, opaqueSkinCanvas, skinLayout[version]["armLS"][0]); | |
} else { | |
box = new THREE.BoxGeometry(4, 12, 4, 4, 12, 4).translate(0, -4, 0); | |
leftArmMesh = colorFaces(box, opaqueSkinCanvas, skinLayout[version]["armL"][0]); | |
} | |
leftArmGroup.add(leftArmMesh); | |
if (version >= 1 && hasAlpha) { | |
let leftSleeveMesh; | |
if (slim) { | |
box = new THREE.BoxGeometry( | |
3.5 + EPSILON * 4, | |
12.5 + EPSILON * 4, | |
4.5 + EPSILON * 4, | |
3, | |
12, | |
4 | |
).translate(0, -4, 0); | |
leftSleeveMesh = colorFaces( | |
box, | |
transparentSkinCanvas, | |
skinLayout[version]["armLS"][1] | |
); | |
} else { | |
box = new THREE.BoxGeometry( | |
4.5 + EPSILON * 4, | |
12.5 + EPSILON * 4, | |
4.5 + EPSILON * 4, | |
4, | |
12, | |
4 | |
).translate(0, -4, 0); | |
leftSleeveMesh = colorFaces(box, transparentSkinCanvas, skinLayout[version]["armL"][1]); | |
} | |
leftSleeveMesh && leftArmGroup.add(leftSleeveMesh); | |
} | |
let rightLegGroup = new THREE.Object3D(); | |
rightLegGroup.position.x = -2; | |
rightLegGroup.position.y = -4; | |
rightLegGroup.position.z = 0; | |
box = new THREE.BoxGeometry(4, 12, 4, 4, 12, 4).translate(0, -6, 0); | |
let rightLegMesh = colorFaces(box, opaqueSkinCanvas, skinLayout[version]["legR"][0]); | |
rightLegGroup.add(rightLegMesh); | |
if (version >= 1 && hasAlpha) { | |
box = new THREE.BoxGeometry( | |
4.5 + EPSILON * 2, | |
12.5 + EPSILON * 2, | |
4.5 + EPSILON * 2, | |
4, | |
12, | |
4 | |
).translate(0, -6, 0); | |
let rightPantMesh = colorFaces(box, transparentSkinCanvas, skinLayout[version]["legR"][1]); | |
rightPantMesh && rightLegGroup.add(rightPantMesh); | |
} | |
let leftLegGroup = new THREE.Object3D(); | |
leftLegGroup.position.x = 2; | |
leftLegGroup.position.y = -4; | |
leftLegGroup.position.z = 0; | |
box = new THREE.BoxGeometry(4, 12, 4, 4, 12, 4).translate(0, -6, 0); | |
let leftLegMesh = colorFaces(box, opaqueSkinCanvas, skinLayout[version]["legL"][0]); | |
leftLegGroup.add(leftLegMesh); | |
if (version >= 1 && hasAlpha) { | |
box = new THREE.BoxGeometry( | |
4.5 + EPSILON * 3, | |
12.5 + EPSILON * 3, | |
4.5 + EPSILON * 3, | |
4, | |
12, | |
4 | |
).translate(0, -6, 0); | |
let leftPantMesh = colorFaces(box, transparentSkinCanvas, skinLayout[version]["legL"][1]); | |
leftPantMesh && leftLegGroup.add(leftPantMesh); | |
} | |
let playerGroup = new THREE.Object3D(); | |
playerGroup.add(headGroup); | |
playerGroup.add(torsoGroup); | |
playerGroup.add(rightArmGroup); | |
playerGroup.add(leftArmGroup); | |
playerGroup.add(rightLegGroup); | |
playerGroup.add(leftLegGroup); | |
if (capeImage) { | |
let capeCanvas = makeOpaque(capeImage); | |
let capeGroup = new THREE.Object3D(); | |
capeGroup.position.x = 0; | |
capeGroup.position.y = 8; | |
capeGroup.position.z = -2; | |
capeGroup.rotation.y += radians(180); | |
let capeMesh; | |
box = new THREE.BoxGeometry(10, 16, 1, 10 * cs, 16 * cs, 1 * cs).translate(0, -8, 0.5); | |
capeMesh = colorFaces(box, capeCanvas, { | |
left: { x: 11 * cs, y: 1 * cs, w: 1 * cs, h: 16 * cs }, | |
right: { x: 0 * cs, y: 1 * cs, w: 1 * cs, h: 16 * cs }, | |
top: { x: 1 * cs, y: 0 * cs, w: 10 * cs, h: 1 * cs }, | |
bottom: { x: 11 * cs, y: cs - 1, w: 10 * cs, h: -1 * cs }, | |
front: { x: 1 * cs, y: 1 * cs, w: 10 * cs, h: 16 * cs }, | |
back: { x: 12 * cs, y: 1 * cs, w: 10 * cs, h: 16 * cs }, | |
}); | |
capeGroup.add(capeMesh); | |
playerGroup.add(capeGroup); | |
} | |
if (flip) { | |
playerGroup.rotation.z += radians(180); | |
} | |
return playerGroup; | |
} | |
let renderState; | |
let renderButton; | |
let skinButtons; | |
let canvas3d; | |
function toggleAnimation(e) { | |
if (renderState.animate) { | |
renderState.animate = false; | |
e.children[0].classList.remove("fa-pause"); | |
e.children[0].classList.add("fa-play"); | |
canvas3d.parent().addClass("animation-paused"); | |
} else { | |
renderState.animate = true; | |
e.children[0].classList.remove("fa-play"); | |
e.children[0].classList.add("fa-pause"); | |
canvas3d.parent().removeClass("animation-paused"); | |
window.requestAnimationFrame(renderAnimation); | |
} | |
let date = new Date(); | |
date.setFullYear(date.getFullYear() + 2); | |
document.cookie = | |
"animate=" + | |
renderState.animate + | |
"; domain=.namemc.com; path=/; expires=" + | |
date.toUTCString(); | |
} | |
function renderAnimation() { | |
if (renderState?.animate) { | |
renderState.frame++; | |
render(); | |
window.requestAnimationFrame(renderAnimation); | |
} | |
} | |
function render() { | |
let time = (4 * renderState.frame) % 360; | |
canvas3d.attr("data-time", time); | |
renderButton.attr( | |
"href", | |
"https://render.namemc.com/skin/3d/body.png?" + | |
"skin=" + | |
canvas3d.attr("data-skin-hash") + | |
"&model=" + | |
canvas3d.attr("data-model") + | |
"&theta=" + | |
canvas3d.attr("data-theta") + | |
"&phi=" + | |
canvas3d.attr("data-phi") + | |
"&time=" + | |
canvas3d.attr("data-time") + | |
"&width=600" + | |
"&height=800" | |
); | |
let angle = Math.sin(radians(time)); | |
renderState.model.children[2].rotation.x = -radians(18) * angle; | |
renderState.model.children[3].rotation.x = radians(18) * angle; | |
renderState.model.children[4].rotation.x = radians(20) * angle; | |
renderState.model.children[5].rotation.x = -radians(20) * angle; | |
if (renderState.model.children[6]) { | |
let capeAngle = Math.sin(radians(renderState.frame)); | |
renderState.model.children[6].rotation.x = radians(18) - radians(6) * capeAngle; | |
} | |
renderState.renderer.render(renderState.scene, renderState.camera); | |
if (renderState.canvas !== renderState.renderer.domElement) { | |
renderState.canvas.getContext("2d").drawImage(renderState.renderer.domElement, 0, 0); | |
} | |
} | |
function allowRotation(renderState, positionCamera) { | |
function startRotation(t, id) { | |
renderState.dragState[id] = { x: t.screenX, y: t.screenY }; | |
} | |
function rotate(t, id) { | |
if (!renderState.dragState[id]) { | |
return false; | |
} | |
let result = true; | |
renderState.theta -= t.screenX - renderState.dragState[id].x; | |
renderState.phi += t.screenY - renderState.dragState[id].y; | |
renderState.canvas.setAttribute("data-theta", renderState.theta % 360); | |
renderState.canvas.setAttribute("data-phi", renderState.phi % 360); | |
renderButton.attr( | |
"href", | |
"https://render.namemc.com/skin/3d/body.png?" + | |
"skin=" + | |
canvas3d.attr("data-skin-hash") + | |
"&model=" + | |
canvas3d.attr("data-model") + | |
"&theta=" + | |
canvas3d.attr("data-theta") + | |
"&phi=" + | |
canvas3d.attr("data-phi") + | |
"&time=" + | |
canvas3d.attr("data-time") + | |
"&width=600" + | |
"&height=800" | |
); | |
if (renderState.phi < -90) { | |
renderState.phi = -90; | |
result = false; | |
} else if (renderState.phi > 90) { | |
renderState.phi = 90; | |
result = false; | |
} | |
positionCamera(renderState.camera, radians(renderState.theta), radians(renderState.phi)); | |
renderState.renderer.render(renderState.scene, renderState.camera); | |
renderState.dragState[id].x = t.screenX; | |
renderState.dragState[id].y = t.screenY; | |
return result; | |
} | |
function endRotation(t, id) { | |
delete renderState.dragState[id]; | |
} | |
renderState.canvas.onmousedown = function (e) { | |
e.preventDefault(); | |
startRotation(e, "mouse"); | |
}; | |
window.onmousemove = function (e) { | |
rotate(e, "mouse"); | |
}; | |
window.onmouseup = function (e) { | |
endRotation(e, "mouse"); | |
}; | |
renderState.canvas.ontouchstart = function (e) { | |
for (let i = 0; i < e.changedTouches.length; i++) { | |
startRotation(e.changedTouches[i], e.changedTouches[i].identifier); | |
} | |
}; | |
renderState.canvas.ontouchmove = function (e) { | |
let result = false; | |
for (let i = 0; i < e.changedTouches.length; i++) { | |
if (rotate(e.changedTouches[i], e.changedTouches[i].identifier)) { | |
result = true; | |
} else { | |
delete renderState.dragState[e.changedTouches[i].identifier]; | |
} | |
} | |
if (result) { | |
e.preventDefault(); | |
} | |
}; | |
renderState.canvas.ontouchend = renderState.canvas.ontouchcancel = function (e) { | |
for (let i = 0; i < e.changedTouches.length; i++) { | |
endRotation(e.changedTouches[i], e.changedTouches[i].identifier); | |
} | |
}; | |
} | |
let renderer; | |
function renderSkinHelper(canvas, animate, theta, phi, time, model) { | |
if (renderState) { | |
renderState.canvas = canvas; | |
renderState.scene.remove(renderState.model); | |
renderState.model = model; | |
renderState.scene.add(model); | |
render(); | |
return; | |
} | |
if (!renderer) { | |
renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true, antialias: true }); | |
} | |
renderState = { | |
canvas: canvas, | |
animate: animate, | |
model: model, | |
theta: theta, | |
phi: phi, | |
scene: new THREE.Scene(), | |
camera: new THREE.PerspectiveCamera(32, canvas.width / canvas.height, 72 - 20, 72 + 20), | |
//camera: new THREE.OrthographicCamera(-19 * canvas.width / canvas.height, 19 * canvas.width / canvas.height, 19, -19, 1, 100), | |
renderer: renderer, | |
dragState: {}, | |
frame: time / 4, | |
}; | |
let origin = new THREE.Vector3(0, 0, 0); | |
function positionCamera(camera, theta, phi) { | |
let cosPhi = Math.cos(phi); | |
camera.position.x = 72 * cosPhi * Math.sin(theta); | |
camera.position.z = 72 * cosPhi * Math.cos(theta); | |
camera.position.y = 72 * Math.sin(phi); | |
camera.lookAt(origin); | |
} | |
renderState.scene.add(model); | |
positionCamera(renderState.camera, radians(renderState.theta), radians(renderState.phi)); | |
allowRotation(renderState, positionCamera); | |
render(); | |
if (renderState.animate) { | |
window.requestAnimationFrame(renderAnimation); | |
} | |
} | |
let modelCache = {}; | |
function renderSkin(canvas, slim, flip, animate, theta, phi, time, skinHash, capeHash, callback) { | |
let hash = [capeHash, skinHash, slim, flip].join(":"); | |
function handleModel() { | |
try { | |
renderSkinHelper(canvas, animate, theta, phi, time, modelCache[hash]); | |
callback(); | |
} catch (e) { | |
callback(e); | |
} | |
} | |
if (modelCache[hash]) { | |
handleModel(); | |
} else { | |
function handleImages(skinImage, capeImage) { | |
let model = buildMinecraftModel(skinImage, capeImage, slim, flip); | |
if (model) { | |
modelCache[hash] = model; | |
handleModel(); | |
} else { | |
callback(); | |
} | |
} | |
let skinImage = new Image(); | |
skinImage.crossOrigin = ""; | |
skinImage.src = skinHash; | |
skinImage.onload = function () { | |
if (capeHash) { | |
let capeImage = new Image(); | |
capeImage.crossOrigin = ""; | |
capeImage.src = capeHash; | |
capeImage.onload = function () { | |
handleImages(skinImage, capeImage); | |
}; | |
capeImage.onerror = function () { | |
handleImages(skinImage, null); | |
console.error("Error loading " + capeImage.src); | |
}; | |
} else { | |
handleImages(skinImage, null); | |
} | |
}; | |
skinImage.onerror = function () { | |
console.error("Error loading " + skinImage.src); | |
}; | |
} | |
} | |
function drawFullSkin2D(e) { | |
let isCape = e.classList.contains("cape-3d"); | |
let skinHash = e.getAttribute("data-skin-hash"); | |
let capeHash = e.getAttribute("data-cape-hash"); | |
let url = isCape ? capeHash : skinHash; | |
let flip = e.getAttribute("data-flip") === "true"; | |
let image = new Image(); | |
image.crossOrigin = ""; | |
image.src = url; | |
image.onload = function () { | |
let opaque = makeOpaque(image); | |
let ctx = e.getContext("2d"); | |
ctx.save(); | |
ctx.mozImageSmoothingEnabled = false; | |
ctx.webkitImageSmoothingEnabled = false; | |
ctx.msImageSmoothingEnabled = false; | |
ctx.imageSmoothingEnabled = false; | |
if (flip) { | |
ctx.translate(e.width, e.height); | |
ctx.scale(-1, -1); | |
} | |
ctx.translate(e.width / 2, e.height / 2); | |
let scale; | |
if (isCape) { | |
scale = Math.min(Math.floor(e.width / 10), Math.floor(e.height / 16)) - 1; | |
ctx.scale(scale, scale); | |
ctx.drawImage(opaque, 1, 1, 10, 16, -5, -8, 10, 16); | |
} else { | |
scale = Math.min(Math.floor(e.width / 16), Math.floor(e.height / 32)) - 1; | |
ctx.scale(scale, scale); | |
ctx.drawImage(opaque, 8, 8, 8, 8, -4, -16, 8, 8); // face | |
ctx.drawImage(opaque, 20, 20, 8, 12, -4, -8, 8, 12); // chest | |
ctx.drawImage(opaque, 44, 20, 4, 12, -8, -8, 4, 12); // right arm | |
let version = image.height >= 64 ? 1 : 0; | |
if (version === 0) { | |
ctx.save(); | |
ctx.scale(-1, 1); | |
ctx.drawImage(opaque, 44, 20, 4, 12, -8, -8, 4, 12); // left arm | |
ctx.restore(); | |
} else { | |
ctx.drawImage(opaque, 36, 52, 4, 12, 4, -8, 4, 12); // left arm | |
} | |
ctx.drawImage(opaque, 4, 20, 4, 12, -4, 4, 4, 12); // right leg | |
if (version === 0) { | |
ctx.save(); | |
ctx.scale(-1, 1); | |
ctx.drawImage(opaque, 4, 20, 4, 12, -4, 4, 4, 12); // left leg | |
ctx.restore(); | |
} else { | |
ctx.drawImage(opaque, 20, 52, 4, 12, 0, 4, 4, 12); // left leg | |
} | |
if (hasAlphaLayer(image)) { | |
ctx.drawImage(image, 40, 8, 8, 8, -4, -16, 8, 8); // mask | |
if (version >= 1) { | |
ctx.drawImage(image, 20, 36, 8, 12, -4, -8, 8, 12); // jacket | |
ctx.drawImage(image, 44, 36, 4, 12, -8, -8, 4, 12); // right sleeve | |
ctx.drawImage(image, 52, 52, 4, 12, 4, -8, 4, 12); // left sleeve | |
ctx.drawImage(image, 4, 36, 4, 12, -4, 4, 4, 12); // right pant | |
ctx.drawImage(image, 4, 52, 4, 12, 0, 4, 4, 12); // left pant | |
} | |
} | |
} | |
ctx.restore(); | |
}; | |
image.onerror = function () { | |
console.error("Error loading " + image.src); | |
}; | |
} | |
function drawSkin3D() { | |
if (!canvas3d.get(0)) return; | |
let slim = canvas3d.attr("data-model") === "slim"; | |
let skinHash = canvas3d.attr("data-skin-hash"); | |
let capeHash = canvas3d.attr("data-cape-hash"); | |
let flip = canvas3d.attr("data-flip") === "true"; | |
let animate = canvas3d.attr("data-animate") === "true"; | |
let theta = canvas3d.attr("data-theta") ? parseFloat(canvas3d.attr("data-theta")) : -30; | |
let phi = canvas3d.attr("data-phi") ? parseFloat(canvas3d.attr("data-phi")) : 20; | |
let time = canvas3d.attr("data-time") ? parseFloat(canvas3d.attr("data-time")) : 90; | |
canvas3d.attr("data-model", slim ? "slim" : "classic"); | |
canvas3d.attr("data-theta", theta); | |
canvas3d.attr("data-phi", phi); | |
canvas3d.attr("data-time", time); | |
renderSkin( | |
canvas3d.get(0), | |
slim, | |
flip, | |
animate, | |
theta, | |
phi, | |
time, | |
skinHash, | |
capeHash, | |
function (err) { | |
if (err) { | |
$(".play-pause-btn").remove(); | |
drawFullSkin2D(canvas3d.get(0)); | |
} | |
} | |
); | |
} | |
function updateSkin(element) { | |
let skinHash = element.getAttribute("data-skin-hash"); | |
let capeHash = element.getAttribute("data-cape-hash"); | |
let model = element.getAttribute("data-model"); | |
let redraw = false; | |
if (skinHash && canvas3d.attr("data-skin-hash") !== skinHash) { | |
canvas3d.attr("data-skin-hash", skinHash); | |
redraw = true; | |
} | |
if (capeHash && canvas3d.attr("data-cape-hash") !== capeHash) { | |
canvas3d.attr("data-cape-hash", capeHash); | |
redraw = true; | |
} | |
if (model && canvas3d.attr("data-model") !== model) { | |
canvas3d.attr("data-model", model); | |
redraw = true; | |
} | |
if (redraw) { | |
drawSkin3D(); | |
skinButtons.each(function (i, e) { | |
$(e).toggleClass( | |
"skin-button-selected", | |
$(e).attr("data-skin-hash") === canvas3d.attr("data-skin-hash") || | |
$(e).attr("data-cape-hash") === canvas3d.attr("data-cape-hash") | |
); | |
}); | |
} | |
return false; | |
} | |
function stopSkin3D() { | |
renderer = undefined; | |
renderState = undefined; | |
} | |
function initSkin3D() { | |
renderer = undefined; | |
renderState = undefined; | |
renderButton = $("#render-button"); | |
skinButtons = $(".skin-button"); | |
canvas3d = $("canvas.skin-3d"); | |
drawSkin3D(); | |
drawSkin2D(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment