Merge pull request #26 from yushijinhun/model-detect
Support model automatic detection
This commit is contained in:
commit
80bf022d4f
|
|
@ -21,7 +21,6 @@ Three.js powered Minecraft skin viewer.
|
||||||
<script>
|
<script>
|
||||||
let skinViewer = new skinview3d.SkinViewer({
|
let skinViewer = new skinview3d.SkinViewer({
|
||||||
domElement: document.getElementById("skin_container"),
|
domElement: document.getElementById("skin_container"),
|
||||||
slim: true,
|
|
||||||
width: 600,
|
width: 600,
|
||||||
height: 600,
|
height: 600,
|
||||||
skinUrl: "img/skin.png",
|
skinUrl: "img/skin.png",
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,15 @@
|
||||||
<script>
|
<script>
|
||||||
let skinViewer = new skinview3d.SkinViewer({
|
let skinViewer = new skinview3d.SkinViewer({
|
||||||
domElement: document.getElementById("skin_container"),
|
domElement: document.getElementById("skin_container"),
|
||||||
slim: true,
|
|
||||||
width: 400,
|
width: 400,
|
||||||
height: 400,
|
height: 400,
|
||||||
skinUrl: "./1_8_texturemap_redux.png"
|
skinUrl: "./1_8_texturemap_redux.png"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// By default, the skin model is automatically detected. You can turn it off in this way:
|
||||||
|
// skinViewer.detectModel = false;
|
||||||
|
// skinViewer.playerObject.skin.slim = true;
|
||||||
|
|
||||||
let control = new skinview3d.createOrbitControls(skinViewer);
|
let control = new skinview3d.createOrbitControls(skinViewer);
|
||||||
skinViewer.animation = new skinview3d.CompositeAnimation();
|
skinViewer.animation = new skinview3d.CompositeAnimation();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
export { SkinObject, CapeObject, PlayerObject } from "./model";
|
export {
|
||||||
export { SkinViewer } from "./viewer";
|
SkinObject,
|
||||||
export { OrbitControls, createOrbitControls } from "./orbit_controls";
|
CapeObject,
|
||||||
|
PlayerObject
|
||||||
|
} from "./model";
|
||||||
|
|
||||||
|
export {
|
||||||
|
SkinViewer
|
||||||
|
} from "./viewer";
|
||||||
|
|
||||||
|
export {
|
||||||
|
OrbitControls,
|
||||||
|
createOrbitControls
|
||||||
|
} from "./orbit_controls";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
invokeAnimation,
|
invokeAnimation,
|
||||||
CompositeAnimation,
|
CompositeAnimation,
|
||||||
|
|
@ -8,3 +20,7 @@ export {
|
||||||
RunningAnimation,
|
RunningAnimation,
|
||||||
RotatingAnimation
|
RotatingAnimation
|
||||||
} from "./animation";
|
} from "./animation";
|
||||||
|
|
||||||
|
export {
|
||||||
|
isSlimSkin
|
||||||
|
} from "./utils";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
function copyImage(context, sX, sY, w, h, dX, dY, flipHorizontal) {
|
||||||
|
let imgData = context.getImageData(sX, sY, w, h);
|
||||||
|
if (flipHorizontal) {
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
for (let x = 0; x < (w / 2); x++) {
|
||||||
|
let index = (x + y * w) * 4;
|
||||||
|
let index2 = ((w - x - 1) + y * w) * 4;
|
||||||
|
let pA1 = imgData.data[index];
|
||||||
|
let pA2 = imgData.data[index + 1];
|
||||||
|
let pA3 = imgData.data[index + 2];
|
||||||
|
let pA4 = imgData.data[index + 3];
|
||||||
|
|
||||||
|
let pB1 = imgData.data[index2];
|
||||||
|
let pB2 = imgData.data[index2 + 1];
|
||||||
|
let pB3 = imgData.data[index2 + 2];
|
||||||
|
let pB4 = imgData.data[index2 + 3];
|
||||||
|
|
||||||
|
imgData.data[index] = pB1;
|
||||||
|
imgData.data[index + 1] = pB2;
|
||||||
|
imgData.data[index + 2] = pB3;
|
||||||
|
imgData.data[index + 3] = pB4;
|
||||||
|
|
||||||
|
imgData.data[index2] = pA1;
|
||||||
|
imgData.data[index2 + 1] = pA2;
|
||||||
|
imgData.data[index2 + 2] = pA3;
|
||||||
|
imgData.data[index2 + 3] = pA4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.putImageData(imgData, dX, dY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTransparency(context, x0, y0, w, h) {
|
||||||
|
let imgData = context.getImageData(x0, y0, w, h);
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
let offset = (x + y * w) * 4;
|
||||||
|
if (imgData.data[offset + 3] !== 0xff) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSkinScale(width) {
|
||||||
|
return width / 64.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertSkinTo1_8(context, width) {
|
||||||
|
let scale = computeSkinScale(width);
|
||||||
|
let copySkin = (sX, sY, w, h, dX, dY, flipHorizontal) => copyImage(context, sX * scale, sY * scale, w * scale, h * scale, dX * scale, dY * scale, flipHorizontal);
|
||||||
|
|
||||||
|
copySkin(4, 16, 4, 4, 20, 48, true); // Top Leg
|
||||||
|
copySkin(8, 16, 4, 4, 24, 48, true); // Bottom Leg
|
||||||
|
copySkin(0, 20, 4, 12, 24, 52, true); // Outer Leg
|
||||||
|
copySkin(4, 20, 4, 12, 20, 52, true); // Front Leg
|
||||||
|
copySkin(8, 20, 4, 12, 16, 52, true); // Inner Leg
|
||||||
|
copySkin(12, 20, 4, 12, 28, 52, true); // Back Leg
|
||||||
|
copySkin(44, 16, 4, 4, 36, 48, true); // Top Arm
|
||||||
|
copySkin(48, 16, 4, 4, 40, 48, true); // Bottom Arm
|
||||||
|
copySkin(40, 20, 4, 12, 40, 52, true); // Outer Arm
|
||||||
|
copySkin(44, 20, 4, 12, 36, 52, true); // Front Arm
|
||||||
|
copySkin(48, 20, 4, 12, 32, 52, true); // Inner Arm
|
||||||
|
copySkin(52, 20, 4, 12, 44, 52, true); // Back Arm
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSkinToCanvas(canvas, image) {
|
||||||
|
let isOldFormat = false;
|
||||||
|
if (image.width !== image.height) {
|
||||||
|
if (image.width === 2 * image.height) {
|
||||||
|
isOldFormat = true;
|
||||||
|
} else {
|
||||||
|
throw `Bad skin size: ${image.width}x${image.height}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = canvas.getContext("2d");
|
||||||
|
if (isOldFormat) {
|
||||||
|
let sideLength = image.width;
|
||||||
|
canvas.width = sideLength;
|
||||||
|
canvas.height = sideLength;
|
||||||
|
context.clearRect(0, 0, sideLength, sideLength);
|
||||||
|
context.drawImage(image, 0, 0, sideLength, sideLength / 2.0);
|
||||||
|
convertSkinTo1_8(context, sideLength);
|
||||||
|
} else {
|
||||||
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
context.clearRect(0, 0, image.width, image.height);
|
||||||
|
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCapeToCanvas(canvas, image) {
|
||||||
|
let isOldFormat = false;
|
||||||
|
if (image.width !== 2 * image.height) {
|
||||||
|
if (image.width * 17 == image.height * 22) {
|
||||||
|
// width/height = 22/17
|
||||||
|
isOldFormat = true;
|
||||||
|
} else {
|
||||||
|
throw `Bad cape size: ${image.width}x${image.height}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = canvas.getContext("2d");
|
||||||
|
if (isOldFormat) {
|
||||||
|
let width = image.width * 64 / 22;
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = width / 2;
|
||||||
|
} else {
|
||||||
|
canvas.width = image.width;
|
||||||
|
canvas.height = image.height;
|
||||||
|
}
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
context.drawImage(image, 0, 0, image.width, image.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSlimSkin(canvasOrImage) {
|
||||||
|
// Detects whether the skin is default or slim.
|
||||||
|
//
|
||||||
|
// The right arm area of *default* skins:
|
||||||
|
// (44,16)->*-------*-------*
|
||||||
|
// (40,20) |top |bottom |
|
||||||
|
// \|/ |4x4 |4x4 |
|
||||||
|
// *-------*-------*-------*-------*
|
||||||
|
// |right |front |left |back |
|
||||||
|
// |4x12 |4x12 |4x12 |4x12 |
|
||||||
|
// *-------*-------*-------*-------*
|
||||||
|
// The right arm area of *slim* skins:
|
||||||
|
// (44,16)->*------*------*-*
|
||||||
|
// (40,20) |top |bottom| |<----[x0=50,y0=16,w=2,h=4]
|
||||||
|
// \|/ |3x4 |3x4 | |
|
||||||
|
// *-------*------*------***-----*-*
|
||||||
|
// |right |front |left |back | |<----[x0=54,y0=20,w=2,h=12]
|
||||||
|
// |4x12 |3x12 |4x12 |3x12 | |
|
||||||
|
// *-------*------*-------*------*-*
|
||||||
|
// Compared with default right arms, slim right arms have 2 unused areas.
|
||||||
|
//
|
||||||
|
// The same is true for left arm:
|
||||||
|
// The left arm area of *default* skins:
|
||||||
|
// (36,48)->*-------*-------*
|
||||||
|
// (32,52) |top |bottom |
|
||||||
|
// \|/ |4x4 |4x4 |
|
||||||
|
// *-------*-------*-------*-------*
|
||||||
|
// |right |front |left |back |
|
||||||
|
// |4x12 |4x12 |4x12 |4x12 |
|
||||||
|
// *-------*-------*-------*-------*
|
||||||
|
// The left arm area of *slim* skins:
|
||||||
|
// (36,48)->*------*------*-*
|
||||||
|
// (32,52) |top |bottom| |<----[x0=42,y0=48,w=2,h=4]
|
||||||
|
// \|/ |3x4 |3x4 | |
|
||||||
|
// *-------*------*------***-----*-*
|
||||||
|
// |right |front |left |back | |<----[x0=46,y0=52,w=2,h=12]
|
||||||
|
// |4x12 |3x12 |4x12 |3x12 | |
|
||||||
|
// *-------*------*-------*------*-*
|
||||||
|
//
|
||||||
|
// If there is a transparent pixel in any of the 4 unused areas, the skin must be slim,
|
||||||
|
// as transparent pixels are not allowed in the first layer.
|
||||||
|
|
||||||
|
if (canvasOrImage instanceof HTMLCanvasElement) {
|
||||||
|
let canvas = canvasOrImage;
|
||||||
|
let scale = computeSkinScale(canvas.width);
|
||||||
|
let context = canvas.getContext("2d");
|
||||||
|
let checkArea = (x, y, w, h) => hasTransparency(context, x * scale, y * scale, w * scale, h * scale);
|
||||||
|
return checkArea(50, 16, 2, 4) ||
|
||||||
|
checkArea(54, 20, 2, 12) ||
|
||||||
|
checkArea(42, 48, 2, 4) ||
|
||||||
|
checkArea(46, 52, 2, 12);
|
||||||
|
} else if (canvasOrImage instanceof HTMLImageElement) {
|
||||||
|
let image = canvasOrImage;
|
||||||
|
let canvas = document.createElement("canvas");
|
||||||
|
loadSkinToCanvas(canvas, image);
|
||||||
|
return isSlimSkin(canvas);
|
||||||
|
} else {
|
||||||
|
throw `Illegal argument: ${canvasOrImage}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isSlimSkin, loadSkinToCanvas, loadCapeToCanvas };
|
||||||
101
src/viewer.js
101
src/viewer.js
|
|
@ -1,61 +1,13 @@
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { PlayerObject } from "./model";
|
import { PlayerObject } from "./model";
|
||||||
import { invokeAnimation } from "./animation";
|
import { invokeAnimation } from "./animation";
|
||||||
|
import { loadSkinToCanvas,loadCapeToCanvas, isSlimSkin } from "./utils";
|
||||||
function copyImage(context, sX, sY, w, h, dX, dY, flipHorizontal) {
|
|
||||||
let imgData = context.getImageData(sX, sY, w, h);
|
|
||||||
if (flipHorizontal) {
|
|
||||||
for (let y = 0; y < h; y++) {
|
|
||||||
for (let x = 0; x < (w / 2); x++) {
|
|
||||||
let index = (x + y * w) * 4;
|
|
||||||
let index2 = ((w - x - 1) + y * w) * 4;
|
|
||||||
let pA1 = imgData.data[index];
|
|
||||||
let pA2 = imgData.data[index + 1];
|
|
||||||
let pA3 = imgData.data[index + 2];
|
|
||||||
let pA4 = imgData.data[index + 3];
|
|
||||||
|
|
||||||
let pB1 = imgData.data[index2];
|
|
||||||
let pB2 = imgData.data[index2 + 1];
|
|
||||||
let pB3 = imgData.data[index2 + 2];
|
|
||||||
let pB4 = imgData.data[index2 + 3];
|
|
||||||
|
|
||||||
imgData.data[index] = pB1;
|
|
||||||
imgData.data[index + 1] = pB2;
|
|
||||||
imgData.data[index + 2] = pB3;
|
|
||||||
imgData.data[index + 3] = pB4;
|
|
||||||
|
|
||||||
imgData.data[index2] = pA1;
|
|
||||||
imgData.data[index2 + 1] = pA2;
|
|
||||||
imgData.data[index2 + 2] = pA3;
|
|
||||||
imgData.data[index2 + 3] = pA4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.putImageData(imgData, dX, dY);
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertSkinTo1_8(context, width) {
|
|
||||||
let scale = width / 64.0;
|
|
||||||
let copySkin = (context, sX, sY, w, h, dX, dY, flipHorizontal) => copyImage(context, sX * scale, sY * scale, w * scale, h * scale, dX * scale, dY * scale, flipHorizontal);
|
|
||||||
|
|
||||||
copySkin(context, 4, 16, 4, 4, 20, 48, true); // Top Leg
|
|
||||||
copySkin(context, 8, 16, 4, 4, 24, 48, true); // Bottom Leg
|
|
||||||
copySkin(context, 0, 20, 4, 12, 24, 52, true); // Outer Leg
|
|
||||||
copySkin(context, 4, 20, 4, 12, 20, 52, true); // Front Leg
|
|
||||||
copySkin(context, 8, 20, 4, 12, 16, 52, true); // Inner Leg
|
|
||||||
copySkin(context, 12, 20, 4, 12, 28, 52, true); // Back Leg
|
|
||||||
copySkin(context, 44, 16, 4, 4, 36, 48, true); // Top Arm
|
|
||||||
copySkin(context, 48, 16, 4, 4, 40, 48, true); // Bottom Arm
|
|
||||||
copySkin(context, 40, 20, 4, 12, 40, 52, true); // Outer Arm
|
|
||||||
copySkin(context, 44, 20, 4, 12, 36, 52, true); // Front Arm
|
|
||||||
copySkin(context, 48, 20, 4, 12, 32, 52, true); // Inner Arm
|
|
||||||
copySkin(context, 52, 20, 4, 12, 44, 52, true); // Back Arm
|
|
||||||
}
|
|
||||||
|
|
||||||
class SkinViewer {
|
class SkinViewer {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.domElement = options.domElement;
|
this.domElement = options.domElement;
|
||||||
this.animation = options.animation || null;
|
this.animation = options.animation || null;
|
||||||
|
this.detectModel = options.animation !== false; // true by default
|
||||||
this.animationPaused = false;
|
this.animationPaused = false;
|
||||||
this.animationTime = 0;
|
this.animationTime = 0;
|
||||||
this.disposed = false;
|
this.disposed = false;
|
||||||
|
|
@ -91,36 +43,16 @@ class SkinViewer {
|
||||||
this.domElement.appendChild(this.renderer.domElement);
|
this.domElement.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
this.playerObject = new PlayerObject(this.layer1Material, this.layer2Material, this.capeMaterial);
|
this.playerObject = new PlayerObject(this.layer1Material, this.layer2Material, this.capeMaterial);
|
||||||
this.playerObject.skin.slim = options.slim === true;
|
|
||||||
this.scene.add(this.playerObject);
|
this.scene.add(this.playerObject);
|
||||||
|
|
||||||
// texture loading
|
// texture loading
|
||||||
this.skinImg.crossOrigin = "anonymous";
|
this.skinImg.crossOrigin = "anonymous";
|
||||||
this.skinImg.onerror = () => console.error("Failed loading " + this.skinImg.src);
|
this.skinImg.onerror = () => console.error("Failed loading " + this.skinImg.src);
|
||||||
this.skinImg.onload = () => {
|
this.skinImg.onload = () => {
|
||||||
let isOldFormat = false;
|
loadSkinToCanvas(this.skinCanvas, this.skinImg);
|
||||||
if (this.skinImg.width !== this.skinImg.height) {
|
|
||||||
if (this.skinImg.width === 2 * this.skinImg.height) {
|
|
||||||
isOldFormat = true;
|
|
||||||
} else {
|
|
||||||
console.error("Bad skin size");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let skinContext = this.skinCanvas.getContext("2d");
|
if (this.detectModel) {
|
||||||
if (isOldFormat) {
|
this.playerObject.skin.slim = isSlimSkin(this.skinCanvas);
|
||||||
let width = this.skinImg.width;
|
|
||||||
this.skinCanvas.width = width;
|
|
||||||
this.skinCanvas.height = width;
|
|
||||||
skinContext.clearRect(0, 0, width, width);
|
|
||||||
skinContext.drawImage(this.skinImg, 0, 0, width, width / 2.0);
|
|
||||||
convertSkinTo1_8(skinContext, width);
|
|
||||||
} else {
|
|
||||||
this.skinCanvas.width = this.skinImg.width;
|
|
||||||
this.skinCanvas.height = this.skinImg.height;
|
|
||||||
skinContext.clearRect(0, 0, this.skinCanvas.width, this.skinCanvas.height);
|
|
||||||
skinContext.drawImage(this.skinImg, 0, 0, this.skinCanvas.width, this.skinCanvas.height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.skinTexture.needsUpdate = true;
|
this.skinTexture.needsUpdate = true;
|
||||||
|
|
@ -133,28 +65,7 @@ class SkinViewer {
|
||||||
this.capeImg.crossOrigin = "anonymous";
|
this.capeImg.crossOrigin = "anonymous";
|
||||||
this.capeImg.onerror = () => console.error("Failed loading " + this.capeImg.src);
|
this.capeImg.onerror = () => console.error("Failed loading " + this.capeImg.src);
|
||||||
this.capeImg.onload = () => {
|
this.capeImg.onload = () => {
|
||||||
let isOldFormat = false;
|
loadCapeToCanvas(this.capeCanvas, this.capeImg);
|
||||||
if (this.capeImg.width !== 2 * this.capeImg.height) {
|
|
||||||
if (this.capeImg.width * 17 == this.capeImg.height * 22) {
|
|
||||||
// width/height = 22/17
|
|
||||||
isOldFormat = true;
|
|
||||||
} else {
|
|
||||||
console.error("Bad cape size");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let capeContext = this.capeCanvas.getContext("2d");
|
|
||||||
if (isOldFormat) {
|
|
||||||
let width = this.capeImg.width * 64 / 22;
|
|
||||||
this.capeCanvas.width = width;
|
|
||||||
this.capeCanvas.height = width / 2;
|
|
||||||
} else {
|
|
||||||
this.capeCanvas.width = this.capeImg.width;
|
|
||||||
this.capeCanvas.height = this.capeImg.height;
|
|
||||||
}
|
|
||||||
capeContext.clearRect(0, 0, this.capeCanvas.width, this.capeCanvas.height);
|
|
||||||
capeContext.drawImage(this.capeImg, 0, 0, this.capeImg.width, this.capeImg.height);
|
|
||||||
|
|
||||||
this.capeTexture.needsUpdate = true;
|
this.capeTexture.needsUpdate = true;
|
||||||
this.capeMaterial.needsUpdate = true;
|
this.capeMaterial.needsUpdate = true;
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ export * from "./model";
|
||||||
export * from "./animation";
|
export * from "./animation";
|
||||||
export * from "./viewer";
|
export * from "./viewer";
|
||||||
export * from "./orbit_controls";
|
export * from "./orbit_controls";
|
||||||
|
export * from "./utils";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export function isSlimSkin(skinImage: HTMLImageElement): boolean;
|
||||||
|
|
@ -5,11 +5,11 @@ import { PlayerObject } from "./model";
|
||||||
export interface SkinViewerOptions {
|
export interface SkinViewerOptions {
|
||||||
domElement: Node;
|
domElement: Node;
|
||||||
animation?: Animation;
|
animation?: Animation;
|
||||||
slim?: boolean;
|
|
||||||
skinUrl?: string;
|
skinUrl?: string;
|
||||||
capeUrl?: string;
|
capeUrl?: string;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
detectModel?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SkinViewer {
|
export class SkinViewer {
|
||||||
|
|
@ -22,6 +22,7 @@ export class SkinViewer {
|
||||||
public animation: Animation;
|
public animation: Animation;
|
||||||
public animationPaused: boolean;
|
public animationPaused: boolean;
|
||||||
public animationTime: number;
|
public animationTime: number;
|
||||||
|
public detectModel: boolean;
|
||||||
public readonly playerObject: PlayerObject;
|
public readonly playerObject: PlayerObject;
|
||||||
public readonly scene: THREE.Scene;
|
public readonly scene: THREE.Scene;
|
||||||
public readonly camera: THREE.PerspectiveCamera;
|
public readonly camera: THREE.PerspectiveCamera;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue