From cd4e54b03800eb2ed2e1caa296da6ea71d5aa19c Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:14:19 +0800 Subject: [PATCH 01/14] add textures resources for example page --- examples/public/img/chainmail_layer_1.png | Bin 0 -> 258 bytes examples/public/img/chainmail_layer_2.png | Bin 0 -> 167 bytes examples/public/img/copper_layer_1.png | Bin 0 -> 872 bytes examples/public/img/copper_layer_2.png | Bin 0 -> 507 bytes examples/public/img/diamond_layer_1.png | Bin 0 -> 448 bytes examples/public/img/diamond_layer_2.png | Bin 0 -> 272 bytes examples/public/img/gold_layer_1.png | Bin 0 -> 434 bytes examples/public/img/gold_layer_2.png | Bin 0 -> 261 bytes examples/public/img/iron_layer_1.png | Bin 0 -> 424 bytes examples/public/img/iron_layer_2.png | Bin 0 -> 243 bytes examples/public/img/netherite_layer_1.png | Bin 0 -> 467 bytes examples/public/img/netherite_layer_2.png | Bin 0 -> 264 bytes examples/public/img/turtle_layer_1.png | Bin 0 -> 278 bytes 13 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/public/img/chainmail_layer_1.png create mode 100644 examples/public/img/chainmail_layer_2.png create mode 100644 examples/public/img/copper_layer_1.png create mode 100644 examples/public/img/copper_layer_2.png create mode 100644 examples/public/img/diamond_layer_1.png create mode 100644 examples/public/img/diamond_layer_2.png create mode 100644 examples/public/img/gold_layer_1.png create mode 100644 examples/public/img/gold_layer_2.png create mode 100644 examples/public/img/iron_layer_1.png create mode 100644 examples/public/img/iron_layer_2.png create mode 100644 examples/public/img/netherite_layer_1.png create mode 100644 examples/public/img/netherite_layer_2.png create mode 100644 examples/public/img/turtle_layer_1.png diff --git a/examples/public/img/chainmail_layer_1.png b/examples/public/img/chainmail_layer_1.png new file mode 100644 index 0000000000000000000000000000000000000000..cd9ed23eb3a1a1c3627f99988ee2460be183f1d7 GIT binary patch literal 258 zcmeAS@N?(olHy`uVBq!ia0vp^4nVBH!VDw>HYaZfQbGYfA+A7r@7}#zwrp9lbjgy1 zOQuYmQl}K&3KV553GxeOaCmkj4ak}2>Eaktack=ZYrew@0xTCgufIFXtK!H&Xx$yat3&z&p6A$iG7XS8NsYSqb4?oN4)jU2E6FOJv zn~J~kuGfv(%{_UQ{=rj{Q?zT+B`$2&Vb?iQ!Isw_bMnsPN2!TI+uSCWimD}QtFBe? zoq8fBL|e-B^QxVUM;#_-nc3cacG)9e+~;-Z>aUj%$zF2SzM11^7Y}qPgQu&X%Q~lo FCIFttWZeJ& literal 0 HcmV?d00001 diff --git a/examples/public/img/chainmail_layer_2.png b/examples/public/img/chainmail_layer_2.png new file mode 100644 index 0000000000000000000000000000000000000000..0d47920d99831bf7e1c9cb65832447fd4052982e GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^4nVBH!VDw>HYaZfQv3lvA+A7r@7}#zwrp9lbjg&7 zQ(VmFPX-DxmIV0)GdMiEkp|?rdAc};RNP9IU|q~0s=;OWfW4`SMKmC-b+e(w4uc0w zXM7Y6oH^6LE#bh(tapHil{u4%TSUV^fZ>b>!}8FIwZ(2sDynP@p%ZxCGoK1n0-DI+ M>FVdQ&MBb@0I>TnE&u=k literal 0 HcmV?d00001 diff --git a/examples/public/img/copper_layer_1.png b/examples/public/img/copper_layer_1.png new file mode 100644 index 0000000000000000000000000000000000000000..70272f3ce647bd8626fff76fba2122a6127ae81e GIT binary patch literal 872 zcmV-u1DE`XP)pF7<5HgbW?9;ba!ELWdLwtX>N2bZe?^J zG%hhNHDpIvQUCw~=1D|BRA@uxmraTkK@djkAbla3@eda|gNh2G8xipa9>9aR62Y~& z5Cn0d2nudo35qKP1!vLv3SaS&MvWGhJ>7J{!;FY8Gb5t2vc_()SRBgB3&)pF_jY<1 zi@s{kCQ&*Qid}yE&~xzkQP1bQcY26bb2f?6nb?4L*LIelZeJbPng(#p`PC_!CJ6Rc zHo)T#TX6?aB*nH3`2OJj_S^v$?YmF$oQkda3IN4|w!Ui4CNWxITWtEG>7l+OP_dnzi;QBXR!iPD+m1wE`~U6Xy}>4yamIgpN0iPfR0&wH#W=1K1|Bt#1rS%C_Lm!HyVsXacCUW&_2yoO z{r>uy-afds-+#W*S2-u8a|&!?;*7yxEXnwv^b~L0rr9j*Hmyl;YVE{= zDt+o`3@|2EB|Rs@EzU7Cp|mqjtg=l!ZJ^L>Mx_UJsb!3|N_tL)cbH8Xr)|O+H{sOl zckC~i;2YDYMzD!hNzY0D3a}wXnN%i>51Y8Q`mqGnWMTL!EwGMjgX2m&H^T|YBhNSs yZsXdAC228IY1V;0Q42HK9FO^&Q!TnVEV>^Hmz2Kx(=H|e0000anMprAyFYeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqp`K}qdj!yA#!62Y z$B+!?r_(n2GCK;m#-1>3(JA7PQQ}&_%(Xz(rNQM5mqtU!lmjo;e(|5bm;cx$)#~C) zlgt10T>pK1^R)8ICR5eArgkK$E{f>X2vfq(Y%Q>fng8Hz+5NcRf38Jv%y{;*rYgtx z#pmc`#RRF(@4x@v#ar;}&$Suv_q?;M_!G`|X_@jxsRKLeZSI_9i(DS@EHeLT*OrOl z%qbJ@GbH%N8D<|{5ccgLOQ(zk$bCbdBig4N0adxmH5<>l&mS zbPi{%RP4IEOxZA=QSwrU@NbtNEfazd12t^?!8^e*zk^46eU+(8>GMbbu71{Je|aJM zlGFqCk6Rzv-M{`~{pRB~@%ra|#cTI|`*{4mvz}~~@1hFM84ott8P2#cC1X}i(!XeX z7iNnm`D}N4cmf}@bw)0)Dtc*rU8QUKhpJ*z`8lR17}$dEC7UW-6<&Tmra$vL?<7&> V9<#Grk-&&z@O1TaS?83{1OT-$*cSi* literal 0 HcmV?d00001 diff --git a/examples/public/img/diamond_layer_1.png b/examples/public/img/diamond_layer_1.png new file mode 100644 index 0000000000000000000000000000000000000000..750b61d927c428a91238766ae34ab1be0d2f5303 GIT binary patch literal 448 zcmV;x0YCnUP)Ea2EM(7r9ivYmFfRR9100d!JMQvg8b*k%9#0ZmCn zK~#9!om0DRgD@0Do!b0C=S=MfAi>(H{e-TmjGPa^*i)wRm=Yu5Tu-4?cI>aLVZ%^I z%9S{SIb6=U=Q!cbK71_AAZ1XZB-Afd~?(jWmW$k0mzBf~ua zW=Z5RqJ2ghfp#x^o8uJ+<%dy?{4L6qlWIxO71-m$VPm0ULshXA4Gta;4lL;J8+K*> zCZk|E6Ze$_|HK=dZWbGo%(GayAL@BS;}fi1%f!KiQ!dueh2!?P!48dlDb;&zNWw@-9(FJJwM}!JG-H!3h5nUH78a`0Bpel*J{Ao}{_V qJ0vYuNtaIZUEdL7t+eppD*ORMY^EU)pidG20000DBGn42eT$n&(Hr>!rvwXGSgYxD*) WX*-|3S7)360000*gf^Q9N@Ar)0Q&Ybqs<40004WQchC7!G6osu0ZQh^{kfGgc$?U-*4-gDv7XOSfhFr$D2E$_)D-*U$rC!O0fzQY1`a-sp zcHpyA^~anf0a4R5FjWov1Hk7bU@BZB0a>0<5&#c6wdr6X!UN#8pvW;BOWG-X`Vhwp z+3BE0&UAWN1#Px~ZNT4Nt_KedpPGj6WC-$l4dTJpG~KSgeCaG%p^5)hRd15TR^ z;yupKihtv>wal`aSsnqB43v^ZE8@IcGQ>e`YcM9Zl|iG8XI>44nc!fUrK^#PVk`s_ zNOkH`a@843V+YA3m&`D5cS>7uYJG|3vuZ7w!&`^CdSuy~c8}Ph7>Fq(ivDkWMx6KB c!*5672a=Y8y9~ahmjD0&07*qoM6N<$f^3|{4*&oF literal 0 HcmV?d00001 diff --git a/examples/public/img/gold_layer_2.png b/examples/public/img/gold_layer_2.png new file mode 100644 index 0000000000000000000000000000000000000000..0d1032e93f320ffadc7f21fd70d22b472b4d8296 GIT binary patch literal 261 zcmeAS@N?(olHy`uVBq!ia0vp^4nVBH!VDw>HYaZfQqloFA+A9B|NsC0{_p+!zwG~q zsQ))?zwVHEu*u-^G^1OkJWW7p#*!evUR!uY4@P-_fLVj#!kWRxYaF=n`o+qGol>bM@iXvo{2W}hghhx|Db#1p z%D}+Oz`b+g4}czvfbVEi1ax~KMF1k$(jvk_qyd0iP}Nk-EuRcM?A7Oqy|}?Z&`)-m z6?a=8IEd%B>n?(mcjx2_4@o|*Ng{-Sm#=G2-?l0S+UkGN4I5{9s&)gY+-%)$sx#&Z zaSg=y>P?b7)ut(~-$k+hykkkxeyOJ4E22n#s+h^l?n2cE*~<$t(&RpjRJ1-CIn#Cw zB8Z0wG6OX0IcJM@3?nWca-(7Nl(I1Dq}jsLuLUoB%HT!dPOfS;H;ZF4nNusx;P)w{)8o}uZPZ#2p3oBt7v(MI^~EBpXFPH3W* S+I?~W0000HYaZfQlbGqA+A9B|Ns9_pFX{L^XA2i z7Y`jew0-;bygVT@paf${kY6x^!?PP{Ku(XRi(^Q|t+y983LZA#V7;(r!kP(!MHx*F z>xB&*1nU0_7L`4>PS$-8f8kF!L%(%--^Q4RdtwS37Kj(J;w{OBm pUp+Z914$dhry>cfH=aN2TJILEC!||6Hyr3L22WQ%mvv4FO#sNdUfuuz literal 0 HcmV?d00001 diff --git a/examples/public/img/netherite_layer_1.png b/examples/public/img/netherite_layer_1.png new file mode 100644 index 0000000000000000000000000000000000000000..11c961e847166a3aa51c54d546ca1ca7febdab81 GIT binary patch literal 467 zcmV;^0WAKBP)#T~S0%NjX11Fgr3QCnO&r5fKpz3JP+!7>fV^00DGTPE!Ct=GbNc00Co3L_t(| z+J%y_Zi6rwg#T7es*a#OK(R?P$C%6sj@6X_c2?TRc&*yP-#6&mM=pdcK;L1WPJ^@V3sM$VhMssZ#RZ~D0s=RrcD`~VTq@DnXVA9*oOao%2)WuEiHvJD+ zRd@-QF#wvL<1Xnz0PtUMXnK)kgX8zl81RxSfJm-aj3s0Za1gti(L=yUhPWWZP51$z zZr$_hRYG&bn2i#6Iuk-hfgiDm9}(ALJUeB)kO`xq;Q9k`X-1=Ep10^d@F_tgHYaZfQgQ)4A+A8WEUPRwEY{c4*WJ$E z+|=AuS64+|K}AwhTu|`IZjoC+6^tc8e!&b5&u*jvIWs(6978H@B}=d_W)NMnly^(h zrkPoDQ;oGJ6=-?*hWq&Y`U@Xgc|b$QrTw3Uj-j;%{P znRrfZWrS$UvR13v44s#@Y$;g7bt-+@d6q?=UOs&~^~56v>uI{iYF(`$uQ7PK`njxg HN@xNA6=GQL literal 0 HcmV?d00001 diff --git a/examples/public/img/turtle_layer_1.png b/examples/public/img/turtle_layer_1.png new file mode 100644 index 0000000000000000000000000000000000000000..1ad88308f4e0bc0f7c70219621d8fee9ae0c298a GIT binary patch literal 278 zcmeAS@N?(olHy`uVBq!ia0vp^4nVBH!VDw>HYaZfQi=gSA+G=b{|7SN_j}ndakA*O z(ya9~D6&^gG0wGDl#VuxGZ51Lnk5cY!Bi6D7tFv=HuQ+{5Gxqx$z7+0nO zTkkWi4PJqw8qY&S1tO#E6HWepZ!SA0!dPw-)3fjUjfV*<&oe&y5%9{&NO|&6o*$lf zdOyBA;_q#LMv@~@P~YgJjFSMT&j XWa8_Zm*2Dmx|YGy)z4*}Q$iB}y1Qh$ literal 0 HcmV?d00001 From f960f8e8b22cdfeedba8ab353c27691a61bf2f8d Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:16:55 +0800 Subject: [PATCH 02/14] Update index.html --- examples/index.html | 72 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/examples/index.html b/examples/index.html index 46ff0d32..03eb6a26 100644 --- a/examples/index.html +++ b/examples/index.html @@ -288,6 +288,78 @@

Cape

+
+

Armors

+
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+
+

Ears

From b21f7fe8a65623b1868c0991fe7167c882b59c42 Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:17:56 +0800 Subject: [PATCH 03/14] Update main.ts --- examples/main.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/examples/main.ts b/examples/main.ts index c52cbad4..51b68720 100644 --- a/examples/main.ts +++ b/examples/main.ts @@ -17,7 +17,6 @@ const availableAnimations = { }; let skinViewer: skinview3d.SkinViewer; - function obtainTextureUrl(id: string): string { const urlInput = document.getElementById(id) as HTMLInputElement; const fileInput = document.getElementById(`${id}_upload`) as HTMLInputElement; @@ -42,6 +41,7 @@ function obtainTextureUrl(id: string): string { } function reloadSkin(): void { + window.skinViewer = skinViewer; //Set window object to debug in console. const input = document.getElementById("skin_url") as HTMLInputElement; const url = obtainTextureUrl("skin_url"); if (url === "") { @@ -50,7 +50,6 @@ function reloadSkin(): void { } else { const skinModel = document.getElementById("skin_model") as HTMLSelectElement; const earsSource = document.getElementById("ears_source") as HTMLSelectElement; - skinViewer .loadSkin(url, { model: skinModel?.value as ModelType, @@ -84,6 +83,36 @@ function reloadCape(): void { } } +function reloadArmors(): void { + const input1 = document.getElementById("helme_url") as HTMLInputElement; + const url1 = obtainTextureUrl("helme_url"); + const input2 = document.getElementById("chestplate_url") as HTMLInputElement; + const url2 = obtainTextureUrl("chestplate_url"); + const input3 = document.getElementById("leggings_url") as HTMLInputElement; + const url3 = obtainTextureUrl("leggings_url"); + const input4 = document.getElementById("boots_url") as HTMLInputElement; + const url4 = obtainTextureUrl("boots_url"); + const textures = [url1, url2, url3, url4]; + const inputs = [input1.input2, input3, input4]; + textures.forEach((url, index) => { + if (url === "") { + inputs[index]?.setCustomValidity(""); + textures[index] = null; + } + }); + skinViewer + .loadArmors(...textures) + ?.then(() => { + inputs.forEach(input => { + input?.setCustomValidity(""); + }); + }) + ?.catch(e => { + input4?.setCustomValidity("One of the 4 images can't be loaded."); + console.error(e); + }); +} + function reloadEars(skipSkinReload = false): void { const earsSource = document.getElementById("ears_source") as HTMLSelectElement; const sourceType = earsSource?.value; @@ -398,12 +427,20 @@ function initializeControls(): void { initializeUploadButton("skin_url", reloadSkin); initializeUploadButton("cape_url", reloadCape); + initializeUploadButton("helme_url", reloadArmors); + initializeUploadButton("chestplate_url", reloadArmors); + initializeUploadButton("leggings_url", reloadArmors); + initializeUploadButton("boots_url", reloadArmors); initializeUploadButton("ears_url", reloadEars); initializeUploadButton("panorama_url", reloadPanorama); const skinUrl = document.getElementById("skin_url") as HTMLInputElement; const skinModel = document.getElementById("skin_model") as HTMLSelectElement; const capeUrl = document.getElementById("cape_url") as HTMLInputElement; + const helmeUrl = document.getElementById("helme_url") as HTMLInputElement; + const chestplateUrl = document.getElementById("chestplate_url") as HTMLInputElement; + const leggingsUrl = document.getElementById("leggings_url") as HTMLInputElement; + const bootsUrl = document.getElementById("boots_url") as HTMLInputElement; const earsSource = document.getElementById("ears_source") as HTMLSelectElement; const earsUrl = document.getElementById("ears_url") as HTMLInputElement; const panoramaUrl = document.getElementById("panorama_url") as HTMLInputElement; @@ -411,6 +448,10 @@ function initializeControls(): void { skinUrl?.addEventListener("change", reloadSkin); skinModel?.addEventListener("change", reloadSkin); capeUrl?.addEventListener("change", reloadCape); + helmeUrl?.addEventListener("change", reloadArmors); + chestplateUrl?.addEventListener("change", reloadArmors); + leggingsUrl?.addEventListener("change", reloadArmors); + bootsUrl?.addEventListener("change", reloadArmors); earsSource?.addEventListener("change", () => reloadEars()); earsUrl?.addEventListener("change", () => reloadEars()); panoramaUrl?.addEventListener("change", reloadPanorama); @@ -511,6 +552,7 @@ function initializeViewer(): void { reloadSkin(); reloadCape(); + reloadArmors(); reloadEars(true); reloadPanorama(); reloadNameTag(); From 2ab5ca48e44b673b2ffeff0ced2eeff3469cb0b5 Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:20:52 +0800 Subject: [PATCH 04/14] Update README.md --- README.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c5b0a65e..9fbd9216 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,20 @@ Three.js powered Minecraft skin viewer. // Load an elytra (from a cape texture) skinViewer.loadCape("img/cape.png", { backEquipment: "elytra" }); - // Unload(hide) the cape / elytra + // Unload (hide) the cape / elytra skinViewer.loadCape(null); + // Load armors + skinViewer.loadArmors( + "img/turtle_layer_1.png", // helmet (main) + "img/diamond_layer_1.png", // chestplate (main) + "img/gold_layer_2.png", // leggings (legs) + "img/iron_layer_1.png" // boots (main) + ); + + // Unload (hide) the armors + skinViewer.loadArmors(null); + // Set the background color skinViewer.background = 0x5a76f3; @@ -93,6 +104,87 @@ skinViewer.globalLight.intensity = 3; Setting `globalLight.intensity` to `3.0` and `cameraLight.intensity` to `0.0` will completely disable shadows. +## Armors + +skinview3d supports loading armor textures for the player. There are two types of armor materials: + +- **`main`**: Used for helmet, chestplate, and boots. +- **`legs`**: Used for leggings. + +The type of an armor texture is automatically detected by analyzing the top‑left area of the image. +If the texture has colored pixels there, it is considered `main`; if it is fully transparent, black, or white, it is considered `legs`. + +### Loading Armors + +You can load armors using the `loadArmors` method. It accepts different numbers of arguments: + +- **`loadArmors(null)`** – Hides all armor pieces. +- **`loadArmors()`** (no arguments) – Also hides all armor (same as passing `null`). +- **`loadArmors(source)`** – A single armor material. + - If the material is detected as `main`, it will be applied to helmet, chestplate, and boots. + - If it is detected as `legs`, it will be applied only to leggings. +- **`loadArmors(main, legs)`** – Two materials. The first should be a `main` texture (for helmet, chestplate, boots) and the second a `legs` texture (for leggings). +- **`loadArmors(helmet, chestplate, leggings, boots)`** – Four materials applied in the exact order: helmet, chestplate, leggings, boots. The third (leggings) must be a `legs` texture; the others must be `main` textures. + +All parameters can be a `RemoteImage` (URL string), a `TextureSource` (HTML image element or canvas), or `null` to leave that slot empty. + +Examples: + +```js +// Clear armors +skinViewer.loadArmors(null); +skinViewer.loadArmors(); // same effect + +// Single main material – equips helmet, chestplate, boots +skinViewer.loadArmors("img/diamond_layer_1.png"); + +// Single legs material – equips leggings only +skinViewer.loadArmors("img/diamond_layer_2.png"); + +// Two materials: main and legs +skinViewer.loadArmors("img/diamond_layer_1.png", "img/diamond_layer_2.png"); + +// Four materials: helmet, chestplate, leggings, boots +skinViewer.loadArmors( + "img/turtle_layer_1.png", // helmet (main) + "img/diamond_layer_1.png", // chestplate (main) + "img/gold_layer_2.png", // leggings (legs) + "img/iron_layer_1.png" // boots (main) +); +``` + +### Using Armors in the Constructor + +You can specify armors directly in the `SkinViewer` options via the `armors` property. +It accepts the same values as the `loadArmors` method: a single material, `null`, or an array of materials. + +```js +new skinview3d.SkinViewer({ + skin: "img/skin.png", + armors: [ + "img/turtle_layer_1.png", // helmet + "img/diamond_layer_1.png", // chestplate + "img/gold_layer_2.png", // leggings + "img/iron_layer_1.png" // boots + ] +}); +``` + +### Loading Armors Together with a Skin + +The `loadSkin` method also accepts an `armors` option, which behaves identically to the constructor option. + +```js +skinViewer.loadSkin("img/skin.png", { + armors: [ + "img/turtle_layer_1.png", + "img/diamond_layer_1.png", + "img/gold_layer_2.png", + "img/iron_layer_1.png" + ] +}); +``` + ## Ears skinview3d supports two types of ear texture: * `standalone`: 14x7 image that contains the ear ([example](https://github.com/bs-community/skinview3d/blob/master/examples/public/img/ears.png)) From afe2b20a8b2d686a4f2dbc466702a7a6b66797c2 Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:23:36 +0800 Subject: [PATCH 05/14] Update model.ts --- src/model.ts | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/src/model.ts b/src/model.ts index c103fbd3..db4f5ad5 100644 --- a/src/model.ts +++ b/src/model.ts @@ -57,6 +57,43 @@ function setUVs( uvAttr.set(new Float32Array(newUVData)); uvAttr.needsUpdate = true; } +function mirrorUVs(geometry: BoxGeometry) { + const uvAttr = geometry.attributes.uv; + const uvArray = uvAttr.array; // Float32Array + const floatsPerFace = 8; // 4 vertices * 2 + const faceCount = 6; + const mirroredFaces = []; + for (let faceIdx = 0; faceIdx < faceCount; faceIdx++) { + const start = faceIdx * floatsPerFace; + const uValues = []; + for (let i = 0; i < 4; i++) { + uValues.push(uvArray[start + i * 2]); + } + const minU = Math.min(...uValues); + const maxU = Math.max(...uValues); + const faceUVs = []; + for (let i = 0; i < 4; i++) { + const oldU = uvArray[start + i * 2]; + const oldV = uvArray[start + i * 2 + 1]; + const newU = minU + maxU - oldU; + faceUVs.push(newU, oldV); + } + mirroredFaces.push(faceUVs); + } + + [mirroredFaces[0], mirroredFaces[1]] = [mirroredFaces[1], mirroredFaces[0]]; + + for (let faceIdx = 0; faceIdx < faceCount; faceIdx++) { + const start = faceIdx * floatsPerFace; + const faceData = mirroredFaces[faceIdx]; + for (let i = 0; i < 4; i++) { + uvArray[start + i * 2] = faceData[i * 2]; + uvArray[start + i * 2 + 1] = faceData[i * 2 + 1]; + } + } + + uvAttr.needsUpdate = true; +} function setSkinUVs(box: BoxGeometry, u: number, v: number, width: number, height: number, depth: number): void { setUVs(box, u, v, width, height, depth, 64, 64); @@ -467,6 +504,131 @@ export class EarsObject extends Group { } } +export class ArmorsObject extends Group { + readonly headArmor: Mesh; + readonly leftArmArmor: Mesh; + readonly rightArmArmor: Mesh; + readonly bodyArmor: Mesh; + readonly bodyArmor2: Mesh; + readonly leftLegArmor: Mesh; + readonly leftLegArmor2: Mesh; + readonly rightLegArmor: Mesh; + readonly rightLegArmor2: Mesh; + private armorHelmeMaterial: MeshStandardMaterial; + private armorChestplateMaterial: MeshStandardMaterial; + private armorLeggingsMaterial: MeshStandardMaterial; + private armorBootsMaterial: MeshStandardMaterial; + constructor() { + super(); + this.armorHelmeMaterial = new MeshStandardMaterial({ + side: DoubleSide, + transparent: true, + alphaTest: 1e-5, + }); + this.armorChestplateMaterial = new MeshStandardMaterial({ + side: DoubleSide, + transparent: true, + alphaTest: 1e-5, + }); + this.armorLeggingsMaterial = new MeshStandardMaterial({ + side: DoubleSide, + transparent: true, + alphaTest: 1e-5, + }); + this.armorBootsMaterial = new MeshStandardMaterial({ + side: DoubleSide, + transparent: true, + alphaTest: 1e-5, + }); + const headArmorBox = new BoxGeometry(10, 10, 10); + setSkinUVs(headArmorBox, 0, 0, 8, 8, 8); + this.headArmor = new Mesh(headArmorBox, this.armorHelmeMaterial); + this.headArmor.name = "headArmor"; + this.headArmor.position.y = 4; + + const bodyArmorBox = new BoxGeometry(10, 14, 6); + setSkinUVs(bodyArmorBox, 16, 16, 8, 12, 4); + this.bodyArmor = new Mesh(bodyArmorBox, this.armorChestplateMaterial); + const bodyArmor2Box = new BoxGeometry(9.3, 13.3, 5.3); + setSkinUVs(bodyArmor2Box, 16, 16, 8, 12, 4); + this.bodyArmor2 = new Mesh(bodyArmor2Box, this.armorLeggingsMaterial); + this.bodyArmor.name = "bodyArmor"; + this.bodyArmor2.name = "bodyArmor2"; + + const rightArmArmorBox = new BoxGeometry(6.5, 14.5, 6.5); + this.rightArmArmor = new Mesh(rightArmArmorBox, this.armorChestplateMaterial); + setSkinUVs(rightArmArmorBox, 40, 16, 4, 12, 4); + this.rightArmArmor.name = "rightArmArmor"; + this.rightArmArmor.position.x = -1; + this.rightArmArmor.position.y = -4; + + const leftArmArmorBox = new BoxGeometry(6.5, 14.5, 6.5); + this.leftArmArmor = new Mesh(leftArmArmorBox, this.armorChestplateMaterial); + setSkinUVs(leftArmArmorBox, 40, 16, 4, 12, 4); + mirrorUVs(leftArmArmorBox); + this.leftArmArmor.name = "leftArmArmor"; + this.leftArmArmor.position.x = 1; + this.leftArmArmor.position.y = -4; + + const rightLegArmorBox = new BoxGeometry(5, 13, 5); + setSkinUVs(rightLegArmorBox, 0, 16, 4, 12, 4); + this.rightLegArmor = new Mesh(rightLegArmorBox, this.armorLeggingsMaterial); + + const rightLegArmor2Box = new BoxGeometry(5.5, 14.5, 6.5); + setSkinUVs(rightLegArmor2Box, 0, 16, 4, 12, 4); + this.rightLegArmor2 = new Mesh(rightLegArmor2Box, this.armorBootsMaterial); + + this.rightLegArmor.name = "rightLegArmor"; + this.rightLegArmor2.name = "rightLegArmor2"; + this.rightLegArmor.position.y = -6; + this.rightLegArmor2.position.y = -6; + + const leftLegArmorBox = new BoxGeometry(5, 13, 5); + setSkinUVs(leftLegArmorBox, 0, 16, 4, 12, 4); + this.leftLegArmor = new Mesh(leftLegArmorBox, this.armorLeggingsMaterial); + mirrorUVs(leftLegArmorBox); + + const leftLegArmor2Box = new BoxGeometry(5.5, 14.5, 6.5); + setSkinUVs(leftLegArmor2Box, 0, 16, 4, 12, 4); + this.leftLegArmor2 = new Mesh(leftLegArmor2Box, this.armorBootsMaterial); + mirrorUVs(leftLegArmor2Box); + this.leftLegArmor.name = "leftLegArmor"; + this.leftLegArmor2.name = "leftLegArmor2"; + this.leftLegArmor.position.y = -6; + this.leftLegArmor2.position.y = -6; + + this.add( + this.headArmor, + this.bodyArmor, + this.bodyArmor2, + this.rightArmArmor, + this.leftArmArmor, + this.rightLegArmor, + this.rightLegArmor2, + this.leftLegArmor, + this.leftLegArmor2 + ); + } + public setArmorMaps(map1: Texture | null, map2: Texture | null, map3: Texture | null, map4: Texture | null): void { + this.armorHelmeMaterial.map = map1; + this.armorHelmeMaterial.needsUpdate = true; + this.armorChestplateMaterial.map = map2; + this.armorChestplateMaterial.needsUpdate = true; + this.armorLeggingsMaterial.map = map3; + this.armorLeggingsMaterial.needsUpdate = true; + this.armorBootsMaterial.map = map4; + this.armorBootsMaterial.needsUpdate = true; + } + get maps(): (Texture | null)[] { + return [ + this.armorHelmeMaterial.map, + this.armorChestplateMaterial.map, + this.armorLeggingsMaterial.map, + this.armorBootsMaterial.map, + ]; + } +} + export type BackEquipment = "cape" | "elytra"; const CapeDefaultAngle = (10.8 * Math.PI) / 180; @@ -476,6 +638,7 @@ export class PlayerObject extends Group { readonly cape: CapeObject; readonly elytra: ElytraObject; readonly ears: EarsObject; + readonly armors: ArmorsObject; constructor() { super(); @@ -506,6 +669,27 @@ export class PlayerObject extends Group { this.ears.position.z = 2 / 3; this.ears.visible = false; this.skin.head.add(this.ears); + + this.armors = new ArmorsObject(); + this.armors.name = "armors"; + this.armors.headArmor.visible = false; + this.armors.bodyArmor.visible = false; + this.armors.bodyArmor2.visible = false; + this.armors.leftArmArmor.visible = false; + this.armors.rightArmArmor.visible = false; + this.armors.rightLegArmor.visible = false; + this.armors.rightLegArmor2.visible = false; + this.armors.leftLegArmor.visible = false; + this.armors.leftLegArmor2.visible = false; + this.skin.head.add(this.armors.headArmor); + this.skin.body.add(this.armors.bodyArmor); + this.skin.body.add(this.armors.bodyArmor2); + this.skin.leftArm.add(this.armors.leftArmArmor); + this.skin.rightArm.add(this.armors.rightArmArmor); + this.skin.rightLeg.add(this.armors.rightLegArmor); + this.skin.rightLeg.add(this.armors.rightLegArmor2); + this.skin.leftLeg.add(this.armors.leftLegArmor); + this.skin.leftLeg.add(this.armors.leftLegArmor2); } get backEquipment(): BackEquipment | null { From a8f6c972599952a9ca93e9344cd8e0560054af0e Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:24:25 +0800 Subject: [PATCH 06/14] Update viewer.ts --- src/viewer.ts | 294 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 292 insertions(+), 2 deletions(-) diff --git a/src/viewer.ts b/src/viewer.ts index eb3af670..7fe17d56 100644 --- a/src/viewer.ts +++ b/src/viewer.ts @@ -32,6 +32,7 @@ import { Clock, Object3D, ColorManagement, + MeshStandardMaterial, } from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js"; @@ -73,6 +74,17 @@ export interface SkinLoadOptions extends LoadOptions { * @defaultValue `false` */ ears?: boolean | "load-only"; + + /** + * The armor textures to load along with the skin. + * + * This option behaves exactly the same as the `armors` option in {@link SkinViewerOptions}. + * You can specify a single armor material, `null` (to hide all armor), or an array of armor materials. + * The array length must be 1, 2, or 4, with the same semantics as described in {@link SkinViewerOptions}. + * + * If unspecified, the armor will not be changed (i.e., remains as previously set). + */ + armors?: RemoteImage | TextureSource | null | Array; } export interface CapeLoadOptions extends LoadOptions { @@ -169,6 +181,26 @@ export interface SkinViewerOptions { textureType: "standalone" | "skin"; source: RemoteImage | TextureSource; }; + /** + * The armor textures of the player. + * + * You can specify a single armor material or an array of armor materials. + * An armor material can be a `RemoteImage`, a `TextureSource`, or `null` (to hide that piece). + * + * When a single material is provided, its type (`"main"` for helmet, chestplate, boots; or `"legs"` for leggings) + * will be automatically detected and applied to the corresponding armor pieces. + * + * When an array is provided, its length must be 1, 2, or 4: + * - **Length 1**: Treated the same as providing a single material (auto‑detect type). + * - **Length 2**: Automatically detects the two materials' types (expected one `"main"` and one `"legs"`) and applies them. + * - **Length 4**: Materials are applied in order: `[helmet, chestplate, leggings, boots]`. + * Any element can be `null` to leave that slot empty. + * + * If the option is omitted, set to `null`, or an empty array, no armor will be visible. + * + * @defaultValue `undefined` (no armor) + */ + armors?: RemoteImage | TextureSource | null | Array; /** * Whether to preserve the buffers until manually cleared or overwritten. @@ -290,9 +322,20 @@ export class SkinViewer { readonly fxaaPass: ShaderPass; readonly skinCanvas: HTMLCanvasElement; + + readonly armorHelmetCanvas: HTMLCanvasElement; + readonly armorChestplateCanvas: HTMLCanvasElement; + readonly armorLeggingsCanvas: HTMLCanvasElement; + readonly armorBootsCanvas: HTMLCanvasElement; + private contexts: (CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D)[] = []; + readonly capeCanvas: HTMLCanvasElement; readonly earsCanvas: HTMLCanvasElement; private skinTexture: Texture | null = null; + private armorHelmetTexture: Texture | null = null; + private armorChestplateTexture: Texture | null = null; + private armorLeggingsTexture: Texture | null = null; + private armorBootsTexture: Texture | null = null; private capeTexture: Texture | null = null; private earsTexture: Texture | null = null; private backgroundTexture: Texture | null = null; @@ -335,6 +378,10 @@ export class SkinViewer { this.canvas = options.canvas === undefined ? document.createElement("canvas") : options.canvas; this.skinCanvas = document.createElement("canvas"); + this.armorHelmetCanvas = document.createElement("canvas"); + this.armorChestplateCanvas = document.createElement("canvas"); + this.armorLeggingsCanvas = document.createElement("canvas"); + this.armorBootsCanvas = document.createElement("canvas"); this.capeCanvas = document.createElement("canvas"); this.earsCanvas = document.createElement("canvas"); @@ -390,6 +437,7 @@ export class SkinViewer { this.playerObject.name = "player"; this.playerObject.skin.visible = false; this.playerObject.cape.visible = false; + this.playerObject.armors.visible = false; this.playerWrapper = new Group(); this.playerWrapper.add(this.playerObject); this.scene.add(this.playerWrapper); @@ -409,6 +457,13 @@ export class SkinViewer { ears: options.ears === "current-skin", }); } + if (options.armors !== undefined) { + if (Array.isArray(options.armors)) { + this.loadArmors(...(options.armors as (RemoteImage | TextureSource | null)[])); + } else { + this.loadArmors(options.armors); + } + } if (options.cape !== undefined) { this.loadCape(options.cape); } @@ -505,6 +560,34 @@ export class SkinViewer { this.fxaaPass.material.uniforms["resolution"].value.y = 1 / (this.height * pixelRatio); } + private recreateArmorTexture(): void { + [this.armorHelmetTexture, this.armorChestplateTexture, this.armorLeggingsTexture, this.armorBootsTexture].forEach( + texture => { + if (texture !== null) { + texture.dispose(); + } + } + ); + this.armorHelmetTexture = new CanvasTexture(this.armorHelmetCanvas); + this.armorHelmetTexture.magFilter = NearestFilter; + this.armorHelmetTexture.minFilter = NearestFilter; + this.armorChestplateTexture = new CanvasTexture(this.armorChestplateCanvas); + this.armorChestplateTexture.magFilter = NearestFilter; + this.armorChestplateTexture.minFilter = NearestFilter; + this.armorLeggingsTexture = new CanvasTexture(this.armorLeggingsCanvas); + this.armorLeggingsTexture.magFilter = NearestFilter; + this.armorLeggingsTexture.minFilter = NearestFilter; + this.armorBootsTexture = new CanvasTexture(this.armorBootsCanvas); + this.armorBootsTexture.magFilter = NearestFilter; + this.armorBootsTexture.minFilter = NearestFilter; + this.playerObject.armors.setArmorMaps( + this.armorHelmetTexture, + this.armorChestplateTexture, + this.armorLeggingsTexture, + this.armorBootsTexture + ); + } + private recreateSkinTexture(): void { if (this.skinTexture !== null) { this.skinTexture.dispose(); @@ -535,6 +618,169 @@ export class SkinViewer { this.earsTexture.minFilter = NearestFilter; this.playerObject.ears.map = this.earsTexture; } + loadArmors(empty: null): void | Promise; + loadArmors(single: TextureSource | RemoteImage): void | Promise; + loadArmors(main: TextureSource | RemoteImage, legs: TextureSource | RemoteImage): void | Promise; + loadArmors( + helmetTexture: TextureSource | RemoteImage, + chestplateTexture: TextureSource | RemoteImage, + leggingsTexture: TextureSource | RemoteImage, + bootsTexture: TextureSource | RemoteImage + ): void | Promise; + loadArmors(...textures: (RemoteImage | TextureSource | null)[]): void | Promise; + loadArmors(...textures: (TextureSource | RemoteImage | null)[]) { + if (textures.length === 0 || (textures.length === 1 && textures[0] === null)) { + this.resetArmors(); + return; + } + + let texturesCopy: (TextureSource | RemoteImage | null)[] = [...textures]; + + const syncImages: (TextureSource | null)[] = texturesCopy.map(() => null); + const asyncPromises: Promise[] = []; + + texturesCopy.forEach((src, index) => { + if (src === null) { + syncImages[index] = null; + } else if (isTextureSource(src)) { + syncImages[index] = src; + } else { + asyncPromises.push( + loadImage(src).then(img => { + syncImages[index] = img; + }) + ); + } + }); + + if (asyncPromises.length > 0) { + return Promise.all(asyncPromises).then(() => { + this._applyArmorTextures(syncImages); + }); + } else { + this._applyArmorTextures(syncImages); + } + } + + private inferArmorType(texture: TextureSource): "legs" | "main" { + const canvas = document.createElement("canvas"); + canvas.width = texture.width; + canvas.height = texture.height; + const ctx = canvas.getContext("2d", { willReadFrequently: true }) as CanvasRenderingContext2D; + ctx.drawImage(texture, 0, 0, texture.width, texture.height); + + const scale = canvas.width / 64; + const x = 0; + const y = 0; + const w = Math.floor(32 * scale); + const h = Math.floor(16 * scale); + + const imgData = ctx.getImageData(x, y, w, h); + const data = imgData.data; + const pixelCount = w * h; + + let allTransparent = true; + let allBlack = true; + let allWhite = true; + + for (let i = 0; i < pixelCount; i++) { + const offset = i * 4; + const r = data[offset]; + const g = data[offset + 1]; + const b = data[offset + 2]; + const a = data[offset + 3]; + + if (a !== 0) allTransparent = false; + if (!(r === 0 && g === 0 && b === 0 && a === 255)) allBlack = false; + if (!(r === 255 && g === 255 && b === 255 && a === 255)) allWhite = false; + + if (!allTransparent && !allBlack && !allWhite) { + return "main"; + } + } + + return allTransparent || allBlack || allWhite ? "legs" : "main"; + } + + private _applyArmorTextures(textures: (TextureSource | null)[]): void { + if (textures.length === 1) { + if (textures[0] && this.inferArmorType(textures[0]) === "main") { + textures = [textures[0], textures[0], null, textures[0]]; + } else { + textures = [null, null, textures[0], null]; + } + } else if (textures.length === 2) { + if (textures[0] == null || textures[1] == null) { + const notNullTexture = textures[0] || textures[1]; + if (notNullTexture && this.inferArmorType(notNullTexture) === "main") { + textures = [textures[0] || textures[1], textures[0] || textures[1], null, textures[0] || textures[1]]; + } else { + textures = [null, null, textures[0] || textures[1], null]; + } + } else if (this.inferArmorType(textures[0]) === "main") { + if (textures[1] && this.inferArmorType(textures[1]) != "legs") { + console.warn("Two 'main' textures are provided."); + } + textures = [textures[0], textures[0], textures[1], textures[0]]; + } else { + if (textures[1] && this.inferArmorType(textures[1]) != "main") { + console.warn("Two 'leg' textures are provided."); + } + textures = [textures[1], textures[1], textures[0], textures[1]]; + } + } else if (textures.length != 4) { + const len = textures.length; + const expected = "Expected 0 arguments, 1 argument , 2 arguments, or 4 arguments."; + const received = `Received ${len} argument${len !== 1 ? "s" : ""}.`; + throw new Error(`Invalid arguments. ${expected} ${received}`); + } + + const [helmet, chestplate, leggings, boots] = textures; + if (this.contexts.length === 0) { + this.contexts = [ + this.armorHelmetCanvas.getContext("2d", { willReadFrequently: true }) as CanvasRenderingContext2D, + this.armorChestplateCanvas.getContext("2d", { willReadFrequently: true }) as CanvasRenderingContext2D, + this.armorLeggingsCanvas.getContext("2d", { willReadFrequently: true }) as CanvasRenderingContext2D, + this.armorBootsCanvas.getContext("2d", { willReadFrequently: true }) as CanvasRenderingContext2D, + ]; + } + + [this.armorHelmetCanvas, this.armorChestplateCanvas, this.armorLeggingsCanvas, this.armorBootsCanvas].forEach( + (canvas, index) => { + if (textures[index]) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + let textureType = this.inferArmorType(textures[index]!); + if ((textureType === "main" && index === 2) || (textureType === "legs" && index != 2)) { + console.warn(`Wrong texture is provided. Index ${index}`); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const sideLength = textures[index]!.width; + canvas.width = sideLength; + canvas.height = sideLength; + } + (canvas.getContext("2d") as CanvasRenderingContext2D).clearRect(0, 0, canvas.width, canvas.height); + } + ); + + if (helmet) this.contexts[0].drawImage(helmet as CanvasImageSource, 0, 0, helmet.width, helmet.width / 2); + if (chestplate) + this.contexts[1].drawImage(chestplate as CanvasImageSource, 0, 0, chestplate.width, chestplate.width / 2); + if (leggings) this.contexts[2].drawImage(leggings as CanvasImageSource, 0, 0, leggings.width, leggings.width / 2); + if (boots) this.contexts[3].drawImage(boots as CanvasImageSource, 0, 0, boots.width, boots.width / 2); + + this.recreateArmorTexture(); + this.playerObject.armors.visible = true; + this.playerObject.armors.headArmor && (this.playerObject.armors.headArmor.visible = !!helmet); + this.playerObject.armors.bodyArmor && (this.playerObject.armors.bodyArmor.visible = !!chestplate || !!leggings); + this.playerObject.armors.leftArmArmor && (this.playerObject.armors.leftArmArmor.visible = !!chestplate); + this.playerObject.armors.rightArmArmor && (this.playerObject.armors.rightArmArmor.visible = !!chestplate); + this.playerObject.armors.leftLegArmor && (this.playerObject.armors.leftLegArmor.visible = !!leggings || !!boots); + this.playerObject.armors.rightLegArmor && (this.playerObject.armors.rightLegArmor.visible = !!leggings || !!boots); + this.playerObject.armors.bodyArmor2 && (this.playerObject.armors.bodyArmor2.visible = !!chestplate || !!leggings); + this.playerObject.armors.leftLegArmor2 && (this.playerObject.armors.leftLegArmor2.visible = !!leggings || !!boots); + this.playerObject.armors.rightLegArmor2 && + (this.playerObject.armors.rightLegArmor2.visible = !!leggings || !!boots); + } loadSkin(empty: null): void; loadSkin( @@ -554,7 +800,13 @@ export class SkinViewer { } else { this.playerObject.skin.modelType = options.model; } - + if (options.armors !== undefined) { + if (Array.isArray(options.armors)) { + this.loadArmors(...options.armors); + } else { + this.loadArmors(options.armors); + } + } if (options.makeVisible !== false) { this.playerObject.skin.visible = true; } @@ -574,10 +826,48 @@ export class SkinViewer { return loadImage(source).then(image => this.loadSkin(image, options)); } } - + resetArmors(): void { + this.playerObject.armors.visible = false; + this.playerObject.armors.headArmor && (this.playerObject.armors.headArmor.visible = false); + this.playerObject.armors.bodyArmor && (this.playerObject.armors.bodyArmor.visible = false); + this.playerObject.armors.leftArmArmor && (this.playerObject.armors.leftArmArmor.visible = false); + this.playerObject.armors.rightArmArmor && (this.playerObject.armors.rightArmArmor.visible = false); + this.playerObject.armors.leftLegArmor && (this.playerObject.armors.leftLegArmor.visible = false); + this.playerObject.armors.rightLegArmor && (this.playerObject.armors.rightLegArmor.visible = false); + this.playerObject.armors.bodyArmor2 && (this.playerObject.armors.bodyArmor2.visible = false); + this.playerObject.armors.leftLegArmor2 && (this.playerObject.armors.leftLegArmor2.visible = false); + this.playerObject.armors.rightLegArmor2 && (this.playerObject.armors.rightLegArmor2.visible = false); + this.playerObject.armors.headArmor && + ((this.playerObject.armors.headArmor.material as MeshStandardMaterial).map = null); + this.playerObject.armors.bodyArmor && + ((this.playerObject.armors.bodyArmor.material as MeshStandardMaterial).map = null); + this.playerObject.armors.leftArmArmor && + ((this.playerObject.armors.leftArmArmor.material as MeshStandardMaterial).map = null); + this.playerObject.armors.rightArmArmor && + ((this.playerObject.armors.rightArmArmor.material as MeshStandardMaterial).map = null); + this.playerObject.armors.leftLegArmor && + ((this.playerObject.armors.leftLegArmor.material as MeshStandardMaterial).map = null); + this.playerObject.armors.rightLegArmor && + ((this.playerObject.armors.rightLegArmor.material as MeshStandardMaterial).map = null); + this.playerObject.armors.bodyArmor2 && + ((this.playerObject.armors.bodyArmor2.material as MeshStandardMaterial).map = null); + this.playerObject.armors.leftLegArmor2 && + ((this.playerObject.armors.leftLegArmor2.material as MeshStandardMaterial).map = null); + this.playerObject.armors.rightLegArmor2 && + ((this.playerObject.armors.rightLegArmor2.material as MeshStandardMaterial).map = null); + [this.armorHelmetTexture, this.armorChestplateTexture, this.armorLeggingsTexture, this.armorBootsTexture].forEach( + texture => { + if (texture !== null) { + texture.dispose(); + texture = null; + } + } + ); + } resetSkin(): void { this.playerObject.skin.visible = false; this.playerObject.skin.map = null; + this.resetArmors(); if (this.skinTexture !== null) { this.skinTexture.dispose(); this.skinTexture = null; From 30c85ccdea97fdad4742524ec6045cf217ef67f7 Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:29:58 +0800 Subject: [PATCH 07/14] FIX helmet spelling mistakes --- examples/index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/index.html b/examples/index.html index 03eb6a26..1d89fd78 100644 --- a/examples/index.html +++ b/examples/index.html @@ -291,8 +291,8 @@

Cape

Armors

- - + + @@ -302,9 +302,9 @@

Armors

- - - +
From 1bb5d4b55762eea4f37c0cc76214dd4cdb1a0d52 Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:31:17 +0800 Subject: [PATCH 08/14] FIX helmet spelling mistakes --- examples/main.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/main.ts b/examples/main.ts index 51b68720..61de1437 100644 --- a/examples/main.ts +++ b/examples/main.ts @@ -84,8 +84,8 @@ function reloadCape(): void { } function reloadArmors(): void { - const input1 = document.getElementById("helme_url") as HTMLInputElement; - const url1 = obtainTextureUrl("helme_url"); + const input1 = document.getElementById("helmet_url") as HTMLInputElement; + const url1 = obtainTextureUrl("helmet_url"); const input2 = document.getElementById("chestplate_url") as HTMLInputElement; const url2 = obtainTextureUrl("chestplate_url"); const input3 = document.getElementById("leggings_url") as HTMLInputElement; @@ -427,7 +427,7 @@ function initializeControls(): void { initializeUploadButton("skin_url", reloadSkin); initializeUploadButton("cape_url", reloadCape); - initializeUploadButton("helme_url", reloadArmors); + initializeUploadButton("helmet_url", reloadArmors); initializeUploadButton("chestplate_url", reloadArmors); initializeUploadButton("leggings_url", reloadArmors); initializeUploadButton("boots_url", reloadArmors); @@ -437,7 +437,7 @@ function initializeControls(): void { const skinUrl = document.getElementById("skin_url") as HTMLInputElement; const skinModel = document.getElementById("skin_model") as HTMLSelectElement; const capeUrl = document.getElementById("cape_url") as HTMLInputElement; - const helmeUrl = document.getElementById("helme_url") as HTMLInputElement; + const helmetUrl = document.getElementById("helmet_url") as HTMLInputElement; const chestplateUrl = document.getElementById("chestplate_url") as HTMLInputElement; const leggingsUrl = document.getElementById("leggings_url") as HTMLInputElement; const bootsUrl = document.getElementById("boots_url") as HTMLInputElement; @@ -448,7 +448,7 @@ function initializeControls(): void { skinUrl?.addEventListener("change", reloadSkin); skinModel?.addEventListener("change", reloadSkin); capeUrl?.addEventListener("change", reloadCape); - helmeUrl?.addEventListener("change", reloadArmors); + helmetUrl?.addEventListener("change", reloadArmors); chestplateUrl?.addEventListener("change", reloadArmors); leggingsUrl?.addEventListener("change", reloadArmors); bootsUrl?.addEventListener("change", reloadArmors); From 129e5dc52b0298266f76dfaff2a7df6086c75a69 Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:32:05 +0800 Subject: [PATCH 09/14] FIX helmet spelling mistakes --- src/model.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/model.ts b/src/model.ts index db4f5ad5..851424bc 100644 --- a/src/model.ts +++ b/src/model.ts @@ -514,13 +514,13 @@ export class ArmorsObject extends Group { readonly leftLegArmor2: Mesh; readonly rightLegArmor: Mesh; readonly rightLegArmor2: Mesh; - private armorHelmeMaterial: MeshStandardMaterial; + private armorHelmetMaterial: MeshStandardMaterial; private armorChestplateMaterial: MeshStandardMaterial; private armorLeggingsMaterial: MeshStandardMaterial; private armorBootsMaterial: MeshStandardMaterial; constructor() { super(); - this.armorHelmeMaterial = new MeshStandardMaterial({ + this.armorHelmetMaterial = new MeshStandardMaterial({ side: DoubleSide, transparent: true, alphaTest: 1e-5, @@ -542,7 +542,7 @@ export class ArmorsObject extends Group { }); const headArmorBox = new BoxGeometry(10, 10, 10); setSkinUVs(headArmorBox, 0, 0, 8, 8, 8); - this.headArmor = new Mesh(headArmorBox, this.armorHelmeMaterial); + this.headArmor = new Mesh(headArmorBox, this.armorHelmetMaterial); this.headArmor.name = "headArmor"; this.headArmor.position.y = 4; @@ -610,8 +610,8 @@ export class ArmorsObject extends Group { ); } public setArmorMaps(map1: Texture | null, map2: Texture | null, map3: Texture | null, map4: Texture | null): void { - this.armorHelmeMaterial.map = map1; - this.armorHelmeMaterial.needsUpdate = true; + this.armorHelmetMaterial.map = map1; + this.armorHelmetMaterial.needsUpdate = true; this.armorChestplateMaterial.map = map2; this.armorChestplateMaterial.needsUpdate = true; this.armorLeggingsMaterial.map = map3; @@ -621,7 +621,7 @@ export class ArmorsObject extends Group { } get maps(): (Texture | null)[] { return [ - this.armorHelmeMaterial.map, + this.armorHelmetMaterial.map, this.armorChestplateMaterial.map, this.armorLeggingsMaterial.map, this.armorBootsMaterial.map, From f58301130c09af263986ce1214f6fc714c879697 Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:09:51 +0800 Subject: [PATCH 10/14] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9fbd9216..86e3bc12 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,9 @@ You can load armors using the `loadArmors` method. It accepts different numbers - **`loadArmors(source)`** – A single armor material. - If the material is detected as `main`, it will be applied to helmet, chestplate, and boots. - If it is detected as `legs`, it will be applied only to leggings. -- **`loadArmors(main, legs)`** – Two materials. The first should be a `main` texture (for helmet, chestplate, boots) and the second a `legs` texture (for leggings). +- **`loadArmors(textureA, textureB)`** – Two materials. Their types are automatically detected: + - If both are provided, the material detected as `main` will be used for helmet, chestplate, and boots, and the material detected as `legs` will be used for leggings. + - If one of them is `null`, the non‑null material is treated as a single material and applied according to its type. - **`loadArmors(helmet, chestplate, leggings, boots)`** – Four materials applied in the exact order: helmet, chestplate, leggings, boots. The third (leggings) must be a `legs` texture; the others must be `main` textures. All parameters can be a `RemoteImage` (URL string), a `TextureSource` (HTML image element or canvas), or `null` to leave that slot empty. From 9687195a5bcc816f5771ae6d10df03452e989c6c Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:59:39 +0800 Subject: [PATCH 11/14] Update README.md --- README.md | 89 ++++++++++++++++++++++++++----------------------------- 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 86e3bc12..4bfc2616 100644 --- a/README.md +++ b/README.md @@ -104,71 +104,68 @@ skinViewer.globalLight.intensity = 3; Setting `globalLight.intensity` to `3.0` and `cameraLight.intensity` to `0.0` will completely disable shadows. + ## Armors -skinview3d supports loading armor textures for the player. There are two types of armor materials: +skinview3d supports loading armor textures for the player. Armor textures can be specified as an object with the following optional properties: -- **`main`**: Used for helmet, chestplate, and boots. -- **`legs`**: Used for leggings. +- `helmet`, `chestplate`, `leggings`, `boots`: textures for the corresponding pieces. +- `main`: a texture that will be used for helmet, chestplate, and boots if their specific textures are not provided. +- `legs`: a texture that will be used for leggings if `leggings` is not provided. -The type of an armor texture is automatically detected by analyzing the top‑left area of the image. -If the texture has colored pixels there, it is considered `main`; if it is fully transparent, black, or white, it is considered `legs`. +Each texture can be a `RemoteImage` (URL string), a `TextureSource` (HTML image element or canvas), or `null` to hide that piece. ### Loading Armors -You can load armors using the `loadArmors` method. It accepts different numbers of arguments: - -- **`loadArmors(null)`** – Hides all armor pieces. -- **`loadArmors()`** (no arguments) – Also hides all armor (same as passing `null`). -- **`loadArmors(source)`** – A single armor material. - - If the material is detected as `main`, it will be applied to helmet, chestplate, and boots. - - If it is detected as `legs`, it will be applied only to leggings. -- **`loadArmors(textureA, textureB)`** – Two materials. Their types are automatically detected: - - If both are provided, the material detected as `main` will be used for helmet, chestplate, and boots, and the material detected as `legs` will be used for leggings. - - If one of them is `null`, the non‑null material is treated as a single material and applied according to its type. -- **`loadArmors(helmet, chestplate, leggings, boots)`** – Four materials applied in the exact order: helmet, chestplate, leggings, boots. The third (leggings) must be a `legs` texture; the others must be `main` textures. - -All parameters can be a `RemoteImage` (URL string), a `TextureSource` (HTML image element or canvas), or `null` to leave that slot empty. +You can load armors using the `loadArmors` method. It accepts an object conforming to the structure above, or `null` to hide all armor. Examples: ```js -// Clear armors +// Hide all armor skinViewer.loadArmors(null); -skinViewer.loadArmors(); // same effect -// Single main material – equips helmet, chestplate, boots -skinViewer.loadArmors("img/diamond_layer_1.png"); +// Equip only helmet, chestplate, and boots using the same "main" texture +skinViewer.loadArmors({ main: "img/diamond_layer_1.png" }); -// Single legs material – equips leggings only -skinViewer.loadArmors("img/diamond_layer_2.png"); +// Equip only leggings using a "legs" texture +skinViewer.loadArmors({ legs: "img/diamond_layer_2.png" }); -// Two materials: main and legs -skinViewer.loadArmors("img/diamond_layer_1.png", "img/diamond_layer_2.png"); +// Equip both main and legs textures +skinViewer.loadArmors({ + main: "img/diamond_layer_1.png", + legs: "img/diamond_layer_2.png" +}); -// Four materials: helmet, chestplate, leggings, boots -skinViewer.loadArmors( - "img/turtle_layer_1.png", // helmet (main) - "img/diamond_layer_1.png", // chestplate (main) - "img/gold_layer_2.png", // leggings (legs) - "img/iron_layer_1.png" // boots (main) -); +// Equip all four pieces individually (helmet, chestplate, leggings, boots) +skinViewer.loadArmors({ + helmet: "img/turtle_layer_1.png", + chestplate: "img/diamond_layer_1.png", + leggings: "img/gold_layer_2.png", + boots: "img/iron_layer_1.png" +}); + +// Mix specific pieces with fallback textures +skinViewer.loadArmors({ + helmet: "img/turtle_layer_1.png", + main: "img/diamond_layer_1.png", // used for chestplate and boots + leggings: "img/gold_layer_2.png" +}); ``` ### Using Armors in the Constructor -You can specify armors directly in the `SkinViewer` options via the `armors` property. -It accepts the same values as the `loadArmors` method: a single material, `null`, or an array of materials. +You can specify armors directly in the `SkinViewer` options via the `armors` property. It accepts the same object format as `loadArmors`. ```js new skinview3d.SkinViewer({ skin: "img/skin.png", - armors: [ - "img/turtle_layer_1.png", // helmet - "img/diamond_layer_1.png", // chestplate - "img/gold_layer_2.png", // leggings - "img/iron_layer_1.png" // boots - ] + armors: { + helmet: "img/turtle_layer_1.png", + chestplate: "img/diamond_layer_1.png", + leggings: "img/gold_layer_2.png", + boots: "img/iron_layer_1.png" + } }); ``` @@ -178,12 +175,10 @@ The `loadSkin` method also accepts an `armors` option, which behaves identically ```js skinViewer.loadSkin("img/skin.png", { - armors: [ - "img/turtle_layer_1.png", - "img/diamond_layer_1.png", - "img/gold_layer_2.png", - "img/iron_layer_1.png" - ] + armors: { + main: "img/diamond_layer_1.png", + legs: "img/diamond_layer_2.png" + } }); ``` From 7c7410a798bb078eb9f93879bb89f8263bfa54ee Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:00:04 +0800 Subject: [PATCH 12/14] Update main.ts --- examples/main.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/main.ts b/examples/main.ts index 61de1437..02750493 100644 --- a/examples/main.ts +++ b/examples/main.ts @@ -92,16 +92,21 @@ function reloadArmors(): void { const url3 = obtainTextureUrl("leggings_url"); const input4 = document.getElementById("boots_url") as HTMLInputElement; const url4 = obtainTextureUrl("boots_url"); - const textures = [url1, url2, url3, url4]; + const textures = { + helmet: url1, + chestplate: url2, + leggings: url3, + boots: url4, + }; const inputs = [input1.input2, input3, input4]; - textures.forEach((url, index) => { - if (url === "") { + Object.keys(textures).forEach((key, index) => { + if (textures[key] === "") { inputs[index]?.setCustomValidity(""); - textures[index] = null; + textures[key] = null; } }); skinViewer - .loadArmors(...textures) + .loadArmors(textures) ?.then(() => { inputs.forEach(input => { input?.setCustomValidity(""); From cad7d9108bf346d6805cf5e894f5367e19bdcc37 Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:01:11 +0800 Subject: [PATCH 13/14] Update index.html --- examples/index.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/index.html b/examples/index.html index 1d89fd78..aaacf9c8 100644 --- a/examples/index.html +++ b/examples/index.html @@ -291,7 +291,9 @@

Cape

Armors

- + From be92763253212544f961d3bf0d26dd5c76cddcee Mon Sep 17 00:00:00 2001 From: undefined303 <54739035+undefined303@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:03:45 +0800 Subject: [PATCH 14/14] Update viewer.ts --- src/viewer.ts | 162 +++++++++++++++++++++++--------------------------- 1 file changed, 73 insertions(+), 89 deletions(-) diff --git a/src/viewer.ts b/src/viewer.ts index 7fe17d56..2e61f30a 100644 --- a/src/viewer.ts +++ b/src/viewer.ts @@ -79,12 +79,15 @@ export interface SkinLoadOptions extends LoadOptions { * The armor textures to load along with the skin. * * This option behaves exactly the same as the `armors` option in {@link SkinViewerOptions}. - * You can specify a single armor material, `null` (to hide all armor), or an array of armor materials. - * The array length must be 1, 2, or 4, with the same semantics as described in {@link SkinViewerOptions}. + * You can specify an object containing textures for specific armor pieces. + * The object may include any of the following optional properties: + * - `helmet`, `chestplate`, `leggings`, `boots`: direct textures for each piece. + * - `main`: a texture that will be used for helmet, chestplate, and boots if their specific textures are not provided. + * - `legs`: a texture that will be used for leggings if not provided. * - * If unspecified, the armor will not be changed (i.e., remains as previously set). + * If the option is omitted, all armors will be removed. */ - armors?: RemoteImage | TextureSource | null | Array; + armors?: ArmorTexture; } export interface CapeLoadOptions extends LoadOptions { @@ -109,7 +112,20 @@ export interface EarsLoadOptions extends LoadOptions { */ textureType?: "standalone" | "skin"; } - +type ArmorTexture = Partial<{ + helmet: TextureSource | RemoteImage | null; + chestplate: TextureSource | RemoteImage | null; + leggings: TextureSource | RemoteImage | null; + boots: TextureSource | RemoteImage | null; + main: TextureSource | RemoteImage | null; + legs: TextureSource | RemoteImage | null; +}>; +type NormalizedArmor = { + helmet: TextureSource | RemoteImage | null; + chestplate: TextureSource | RemoteImage | null; + leggings: TextureSource | RemoteImage | null; + boots: TextureSource | RemoteImage | null; +}; export interface SkinViewerOptions { /** * The canvas where the renderer draws its output. @@ -184,23 +200,21 @@ export interface SkinViewerOptions { /** * The armor textures of the player. * - * You can specify a single armor material or an array of armor materials. - * An armor material can be a `RemoteImage`, a `TextureSource`, or `null` (to hide that piece). + * You can specify an object containing textures for different armor pieces. + * The object may include any of the following optional properties: + * - `helmet`, `chestplate`, `leggings`, `boots`: textures for individual pieces. + * - `main`: a texture that will be applied to helmet, chestplate, and boots when their specific properties are absent. + * - `legs`: a texture that will be applied to leggings when `leggings` is absent. * - * When a single material is provided, its type (`"main"` for helmet, chestplate, boots; or `"legs"` for leggings) - * will be automatically detected and applied to the corresponding armor pieces. + * Each texture can be a `RemoteImage` (URL string), a `TextureSource` (HTML image element), or `null` to hide that piece. * - * When an array is provided, its length must be 1, 2, or 4: - * - **Length 1**: Treated the same as providing a single material (auto‑detect type). - * - **Length 2**: Automatically detects the two materials' types (expected one `"main"` and one `"legs"`) and applies them. - * - **Length 4**: Materials are applied in order: `[helmet, chestplate, leggings, boots]`. - * Any element can be `null` to leave that slot empty. + * If a property is omitted, it defaults to `null` (no texture for that piece), but may be overridden by `main` or `legs` as described above. * - * If the option is omitted, set to `null`, or an empty array, no armor will be visible. + * If the option is omitted, set to `null`, or an empty object, all armors will be removed. * * @defaultValue `undefined` (no armor) */ - armors?: RemoteImage | TextureSource | null | Array; + armors?: ArmorTexture; /** * Whether to preserve the buffers until manually cleared or overwritten. @@ -458,11 +472,7 @@ export class SkinViewer { }); } if (options.armors !== undefined) { - if (Array.isArray(options.armors)) { - this.loadArmors(...(options.armors as (RemoteImage | TextureSource | null)[])); - } else { - this.loadArmors(options.armors); - } + this.loadArmors(options.armors); } if (options.cape !== undefined) { this.loadCape(options.cape); @@ -619,35 +629,31 @@ export class SkinViewer { this.playerObject.ears.map = this.earsTexture; } loadArmors(empty: null): void | Promise; - loadArmors(single: TextureSource | RemoteImage): void | Promise; - loadArmors(main: TextureSource | RemoteImage, legs: TextureSource | RemoteImage): void | Promise; - loadArmors( - helmetTexture: TextureSource | RemoteImage, - chestplateTexture: TextureSource | RemoteImage, - leggingsTexture: TextureSource | RemoteImage, - bootsTexture: TextureSource | RemoteImage - ): void | Promise; - loadArmors(...textures: (RemoteImage | TextureSource | null)[]): void | Promise; - loadArmors(...textures: (TextureSource | RemoteImage | null)[]) { - if (textures.length === 0 || (textures.length === 1 && textures[0] === null)) { + loadArmors(textures: ArmorTexture | null): void | Promise; + loadArmors(textures: ArmorTexture | null) { + if (!textures) { this.resetArmors(); return; } - let texturesCopy: (TextureSource | RemoteImage | null)[] = [...textures]; - - const syncImages: (TextureSource | null)[] = texturesCopy.map(() => null); + let texturesCopy: NormalizedArmor = { + helmet: textures.helmet ?? textures.main ?? null, + chestplate: textures.chestplate ?? textures.main ?? null, + leggings: textures.leggings ?? textures.legs ?? null, + boots: textures.boots ?? textures.main ?? null, + }; + const syncImages: ArmorTexture = {}; const asyncPromises: Promise[] = []; - texturesCopy.forEach((src, index) => { - if (src === null) { - syncImages[index] = null; - } else if (isTextureSource(src)) { - syncImages[index] = src; + (Object.keys(texturesCopy) as Array).forEach(key => { + if (texturesCopy[key] === null) { + syncImages[key] = null; + } else if (isTextureSource(texturesCopy[key])) { + syncImages[key] = texturesCopy[key] as TextureSource; } else { asyncPromises.push( - loadImage(src).then(img => { - syncImages[index] = img; + loadImage(texturesCopy[key] as RemoteImage).then(img => { + syncImages[key] = img; }) ); } @@ -655,10 +661,10 @@ export class SkinViewer { if (asyncPromises.length > 0) { return Promise.all(asyncPromises).then(() => { - this._applyArmorTextures(syncImages); + this._applyArmorTextures(syncImages as NormalizedArmor); }); } else { - this._applyArmorTextures(syncImages); + this._applyArmorTextures(syncImages as NormalizedArmor); } } @@ -702,40 +708,28 @@ export class SkinViewer { return allTransparent || allBlack || allWhite ? "legs" : "main"; } - private _applyArmorTextures(textures: (TextureSource | null)[]): void { - if (textures.length === 1) { - if (textures[0] && this.inferArmorType(textures[0]) === "main") { - textures = [textures[0], textures[0], null, textures[0]]; - } else { - textures = [null, null, textures[0], null]; - } - } else if (textures.length === 2) { - if (textures[0] == null || textures[1] == null) { - const notNullTexture = textures[0] || textures[1]; - if (notNullTexture && this.inferArmorType(notNullTexture) === "main") { - textures = [textures[0] || textures[1], textures[0] || textures[1], null, textures[0] || textures[1]]; - } else { - textures = [null, null, textures[0] || textures[1], null]; - } - } else if (this.inferArmorType(textures[0]) === "main") { - if (textures[1] && this.inferArmorType(textures[1]) != "legs") { - console.warn("Two 'main' textures are provided."); - } - textures = [textures[0], textures[0], textures[1], textures[0]]; - } else { - if (textures[1] && this.inferArmorType(textures[1]) != "main") { - console.warn("Two 'leg' textures are provided."); - } - textures = [textures[1], textures[1], textures[0], textures[1]]; + private _applyArmorTextures(textures: NormalizedArmor): void { + const helmet = textures.helmet as TextureSource; + const chestplate = textures.chestplate as TextureSource; + const leggings = textures.leggings as TextureSource; + const boots = textures.boots as TextureSource; + const inferArmorType = this.inferArmorType; + const armorTypeMap: Record = { + helmet: "main", + chestplate: "main", + leggings: "legs", + boots: "main", + }; + + const armorNames = ["helmet", "chestplate", "leggings", "boots"]; + function setInvalidTextureWarn(type: keyof NormalizedArmor) { + if (textures[type] && inferArmorType(textures[type] as TextureSource) != armorTypeMap[type]) { + console.warn(`Invalid texture , from ${type}`); } - } else if (textures.length != 4) { - const len = textures.length; - const expected = "Expected 0 arguments, 1 argument , 2 arguments, or 4 arguments."; - const received = `Received ${len} argument${len !== 1 ? "s" : ""}.`; - throw new Error(`Invalid arguments. ${expected} ${received}`); } - - const [helmet, chestplate, leggings, boots] = textures; + armorNames.forEach(key => { + setInvalidTextureWarn(key as keyof NormalizedArmor); + }); if (this.contexts.length === 0) { this.contexts = [ this.armorHelmetCanvas.getContext("2d", { willReadFrequently: true }) as CanvasRenderingContext2D, @@ -747,14 +741,8 @@ export class SkinViewer { [this.armorHelmetCanvas, this.armorChestplateCanvas, this.armorLeggingsCanvas, this.armorBootsCanvas].forEach( (canvas, index) => { - if (textures[index]) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - let textureType = this.inferArmorType(textures[index]!); - if ((textureType === "main" && index === 2) || (textureType === "legs" && index != 2)) { - console.warn(`Wrong texture is provided. Index ${index}`); - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const sideLength = textures[index]!.width; + if (textures[armorNames[index] as keyof NormalizedArmor]) { + const sideLength = (textures[armorNames[index] as keyof NormalizedArmor] as TextureSource).width; canvas.width = sideLength; canvas.height = sideLength; } @@ -801,11 +789,7 @@ export class SkinViewer { this.playerObject.skin.modelType = options.model; } if (options.armors !== undefined) { - if (Array.isArray(options.armors)) { - this.loadArmors(...options.armors); - } else { - this.loadArmors(options.armors); - } + this.loadArmors(options.armors); } if (options.makeVisible !== false) { this.playerObject.skin.visible = true;