Skip to content

Commit 3e6f7b9

Browse files
authoredNov 21, 2024··
Merge pull request #182 from Benjythebee/add/overlayTextureManager
Working great! :) we just need to update docs, will add a new issue for it :)
2 parents 1187e9b + 0d24410 commit 3e6f7b9

15 files changed

+739
-122
lines changed
 

‎public/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"name": "Anata",
1212
"description": "Male",
1313
"portrait": "./assets/portraitImages/male.jpg",
14-
"manifest":"https://m3-org.github.io/loot-assets/anata/male/manifest.json",
14+
"manifest":"./loot-assets/anata/male/manifest.json",
1515
"format": "vrm"
1616
},
1717
{

‎src/components/decals/decalGrid.jsx

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from "react";
2+
import styles from "../../pages/Appearance.module.css";
3+
import cancel from '../../images/cancel.png';
4+
import {SceneContext} from "../../context/SceneContext";
5+
import CustomButton from "../custom-button";
6+
import {combineURLs} from "../../library/load-utils";
7+
import DecalItem from "./decalItem";
8+
9+
const DecalGridView = ({selectedTraitGroup,onBack})=>{
10+
const {decalManager,characterManager} = React.useContext(SceneContext);
11+
const [selectedDecals, setSelectedDecals] = React.useState([]);
12+
13+
const decals = selectedTraitGroup.getAllDecals();
14+
React.useEffect(()=>{
15+
const selected= Array.from(decalManager.applied.keys())
16+
setSelectedDecals(selected.map((x)=>x));
17+
},[])
18+
19+
20+
return (
21+
<div className={styles["selector-container-column"]}>
22+
<CustomButton
23+
theme="dark"
24+
text={"Back"}
25+
size={14}
26+
className={styles.buttonLeft}
27+
onClick={onBack}
28+
/>
29+
<div className={styles["selector-container"]} >
30+
<DecalItem key={"empty"}
31+
src={cancel}
32+
active={false}
33+
select={()=>{
34+
decalManager.removeAllOverlayedTextures()
35+
setSelectedDecals([]);
36+
}}
37+
/>
38+
{decals.map((decal)=>{
39+
const path = combineURLs(characterManager.manifestData.getTraitsDirectory(),decal.thumbnail);
40+
41+
return (
42+
<DecalItem
43+
key={decal.id}
44+
src={path}
45+
active={selectedDecals.includes(decal.id)}
46+
select={()=>{
47+
if(selectedDecals.includes(decal.id)){
48+
decalManager.removeOverlayTexture(decal.id).then(()=>{
49+
setSelectedDecals(selectedDecals.filter((x)=>x!==decal.id));
50+
})
51+
52+
}else{
53+
decalManager.loadOverlayTexture(selectedTraitGroup,decal.id).then(()=>{
54+
setSelectedDecals(selectedDecals.concat([decal.id]));
55+
})
56+
}
57+
}}
58+
/>
59+
)
60+
})}
61+
</div>
62+
63+
</div>
64+
)
65+
}
66+
67+
68+
export default DecalGridView;

‎src/components/decals/decalItem.jsx

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
import styles from "../../pages/Appearance.module.css"
3+
import {TokenBox} from '../token-box';
4+
5+
const DecalItem = ({active,src,select})=>{
6+
7+
return (
8+
<div
9+
className={`${styles["selectorButton"]}`}
10+
onClick={select}
11+
>
12+
<TokenBox
13+
size={56}
14+
icon={src||''}
15+
rarity={active ? "mythic" : "none"}
16+
/>
17+
</div>
18+
)
19+
}
20+
21+
export default DecalItem;

‎src/context/SceneContext.jsx

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const SceneProvider = (props) => {
1414
const [characterManager, setCharacterManager] = useState(null)
1515
const [loraDataGenerator, setLoraDataGenerator] = useState(null)
1616
const [spriteAtlasGenerator, setSpriteAtlasGenerator] = useState(null)
17+
const [decalManager, setDecalManager] = useState(null)
1718
const [thumbnailsGenerator, setThumbnailsGenerator] = useState(null)
1819
const [sceneElements, setSceneElements] = useState(null)
1920
const [animationManager, setAnimationManager] = useState(null)
@@ -46,6 +47,7 @@ export const SceneProvider = (props) => {
4647
setSceneElements(sceneElements);
4748
setAnimationManager(characterManager.animationManager)
4849
setLookAtManager(characterManager.lookAtManager)
50+
setDecalManager(characterManager.overlayedTextureManager)
4951
setControls(controls);
5052
setLoraDataGenerator(new LoraDataGenerator(characterManager))
5153
setSpriteAtlasGenerator(new SpriteAtlasGenerator(characterManager))
@@ -132,6 +134,7 @@ export const SceneProvider = (props) => {
132134
manifest,
133135
setManifest,
134136
scene,
137+
decalManager,
135138
characterManager,
136139
loraDataGenerator,
137140
spriteAtlasGenerator,

‎src/images/sticker.png

448 Bytes
Loading

‎src/library/CharacterManifestData.js

+256-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
import { getAsArray } from "./utils";
22

3+
/**
4+
* @typedef {Object} TextureCollectionItem
5+
* @property {string} id
6+
* @property {string} name
7+
* @property {string} directory
8+
* @property {string} [fullDirectory]
9+
* @property {string} [thumbnail]
10+
*/
11+
12+
/**
13+
* @typedef {Object} TextureCollection
14+
* @property {string} trait
15+
* @property {string} type
16+
* @property {TextureCollectionItem[]} collection
17+
*
18+
*/
19+
320
export class CharacterManifestData{
421
constructor(manifest){
522
const {
@@ -23,6 +40,7 @@ export class CharacterManifestData{
2340
vrmMeta,
2441
traits,
2542
textureCollections,
43+
decalCollections,
2644
colorCollections,
2745
canDownload = true,
2846
downloadOptions = {}
@@ -105,6 +123,11 @@ export class CharacterManifestData{
105123
this.textureTraitsMap = null;
106124
this.createTextureTraits(textureCollections);
107125

126+
this.decalTraits = [];
127+
this.decalTraitsMap = null;
128+
129+
this.createDecalTraits(decalCollections);
130+
108131
this.colorTraits = [];
109132
this.colorTraitsMap = null;
110133
this.createColorTraits(colorCollections);
@@ -324,6 +347,14 @@ export class CharacterManifestData{
324347
return this.textureTraitsMap.get(groupTraitID);
325348
}
326349

350+
// decals
351+
getDecalTrait(groupTraitID, traitID){
352+
return this.getDecalGroup(groupTraitID)?.getTrait(traitID);
353+
}
354+
getDecalGroup(decalGroupTraitId){
355+
return this.decalTraitsMap.get(decalGroupTraitId);
356+
}
357+
327358
// colors
328359
getColorTrait(groupTraitID, traitID){
329360
return this.getColorGroup(groupTraitID)?.getTrait(traitID);
@@ -360,17 +391,35 @@ export class CharacterManifestData{
360391
return result;
361392
}
362393

394+
getDecalsDirectory(){
395+
let result = (this.assetsLocation || "") + (this.decalDirectory || "");
396+
if (!result.endsWith("/")&&!result.endsWith("\\"))
397+
result += "/";
398+
return result;
399+
}
363400

364401

365402

366403
// Given an array of traits, saves an array of TraitModels
367404
createModelTraits(modelTraits, replaceExisting = false){
368405
if (replaceExisting) this.modelTraits = [];
369406

407+
let hasTraitWithDecals = false
370408
getAsArray(modelTraits).forEach(traitObject => {
371-
this.modelTraits.push(new TraitModelsGroup(this, traitObject))
409+
const group = new TraitModelsGroup(this, traitObject)
410+
this.modelTraits.push(group)
411+
412+
/**
413+
* We only support one group with decals at the moment; if there are multiple groups with decals, we will log a warning
414+
*/
415+
if(hasTraitWithDecals && group.getAllDecals()?.length){
416+
console.warn("Detected multiple traits with decals; only one trait with decals is supported at the moment")
417+
}else if (!group.getAllDecals()?.length){
418+
hasTraitWithDecals = true
419+
}
372420
});
373421

422+
374423
this.modelTraitsMap = new Map(this.modelTraits.map(item => [item.trait, item]));
375424

376425
// Updates all restricted traits for each group models
@@ -394,6 +443,19 @@ export class CharacterManifestData{
394443

395444
this.textureTraitsMap = new Map(this.textureTraits.map(item => [item.trait, item]));
396445
}
446+
/**
447+
* @param {TextureCollection[]} decalTraitGroups
448+
* @param {boolean} [replaceExisting]
449+
*/
450+
createDecalTraits(decalTraitGroups, replaceExisting = false){
451+
if (replaceExisting) this.decalTraits = [];
452+
453+
getAsArray(decalTraitGroups).forEach(traitObject => {
454+
this.decalTraits.push(new DecalTextureGroup(this, traitObject))
455+
});
456+
457+
this.decalTraitsMap = new Map(this.decalTraits.map(item => [item.trait, item]));
458+
}
397459

398460
createColorTraits(colorTraits, replaceExisting = false){
399461
if (replaceExisting) this.colorTraits = [];
@@ -409,7 +471,16 @@ export class CharacterManifestData{
409471

410472

411473
// Must be created AFTER color collections and texture collections have been created
412-
class TraitModelsGroup{
474+
export class TraitModelsGroup{
475+
/**
476+
* @type {ModelTrait[]}
477+
*/
478+
collection
479+
/**
480+
* @type {CharacterManifestData}
481+
*/
482+
manifestData
483+
413484
constructor(manifestData, options){
414485
const {
415486
trait,
@@ -487,6 +558,15 @@ class TraitModelsGroup{
487558
return this.collectionMap.get(traitID);
488559
}
489560

561+
/**
562+
*
563+
* @returns {DecalTrait[]}
564+
*/
565+
getAllDecals(){
566+
const decalGroup = this.collection.map(trait => trait.targetDecalCollection).flat();
567+
return decalGroup.map((c)=>c?.collection).flat().filter((c)=>!!c);
568+
}
569+
490570
getTraitByIndex(index){
491571
return this.collection[index];
492572
}
@@ -509,13 +589,23 @@ class TraitModelsGroup{
509589

510590
}
511591
class TraitTexturesGroup{
592+
/**
593+
*
594+
* @param {CharacterManifestData} manifestData
595+
* @param {TextureCollection} options
596+
*/
512597
constructor(manifestData, options){
513598
const {
514599
trait,
515600
collection
516601
}= options;
602+
if(!trait){
603+
console.warn("TraitTexturesGroup is missing property trait")
604+
this.trait = "undefined"+Math.floor(Math.random()*10)
605+
}else{
606+
this.trait = trait;
607+
}
517608
this.manifestData = manifestData;
518-
this.trait = trait;
519609

520610
this.collection = [];
521611
this.collectionMap = null;
@@ -569,6 +659,86 @@ class TraitTexturesGroup{
569659
null;
570660
}
571661
}
662+
export class DecalTextureGroup{
663+
/**
664+
* @type {string}
665+
*/
666+
trait
667+
/**
668+
* @type {DecalTrait[]}
669+
*/
670+
collection
671+
/**
672+
* @type {Map<string,DecalTrait>}
673+
*/
674+
collectionMap
675+
/**
676+
*
677+
* @param {CharacterManifestData} manifestData
678+
* @param {TextureCollection} options
679+
*/
680+
constructor(manifestData, options){
681+
const {
682+
trait,
683+
collection
684+
}= options;
685+
this.manifestData = manifestData;
686+
if(!trait){
687+
console.warn("DecalTextureGroup is missing property trait")
688+
this.trait = "undefined"+Math.floor(Math.random()*10)
689+
}else{
690+
this.trait = trait;
691+
}
692+
this.collection = [];
693+
this.collectionMap = null;
694+
this.createCollection(collection);
695+
}
696+
697+
appendCollection(decalTraitGroup, replaceExisting=false){
698+
decalTraitGroup.collection.forEach(newTextureTrait => {
699+
const textureTrait = this.getTrait(newTextureTrait.id)
700+
if (textureTrait != null){
701+
// replace only if requested ro replace
702+
if (replaceExisting){
703+
console.log(`Texture with id ${newTextureTrait.id} exists and will be replaced with new one`)
704+
this.collectionMap.set(newTextureTrait.id, newTextureTrait)
705+
const ind = this.collection.indexOf(textureTrait)
706+
this.collection[ind] = newTextureTrait;
707+
}
708+
else{
709+
console.log(`Texture with id ${newTextureTrait.id} exists, skipping`)
710+
}
711+
}
712+
else{
713+
// create
714+
this.collection.push(newTextureTrait)
715+
this.collectionMap.set(newTextureTrait.id, newTextureTrait);
716+
}
717+
});
718+
}
719+
720+
createCollection(itemCollection, replaceExisting = false){
721+
if (replaceExisting) this.collection = [];
722+
getAsArray(itemCollection).forEach(item => {
723+
this.collection.push(new DecalTrait(this, item))
724+
});
725+
this.collectionMap = new Map(this.collection.map(item => [item.id, item]));
726+
}
727+
728+
getTrait(traitID){
729+
return this.collectionMap.get(traitID);
730+
}
731+
732+
getTraitByIndex(index){
733+
return this.collection[index];
734+
}
735+
736+
getRandomTrait(){
737+
return this.collection.length > 0 ?
738+
this.collection[Math.floor(Math.random() * this.collection.length)] :
739+
null;
740+
}
741+
}
572742
class TraitColorsGroup{
573743
constructor(manifestData, options){
574744
const {
@@ -628,8 +798,20 @@ class TraitColorsGroup{
628798
null;
629799
}
630800
}
631-
class ModelTrait{
632-
blendshapeTraits = [];
801+
export class ModelTrait{
802+
blendshapeTraits = [];
803+
/**
804+
* @type {string[]}
805+
* */
806+
decalMeshNameTargets=[]
807+
/**
808+
* @type {DecalTextureGroup | null}
809+
*/
810+
targetDecalCollection=null
811+
/**
812+
* @type {TraitModelsGroup}
813+
*/
814+
traitGroup
633815
blendshapeTraitsMap = new Map();
634816
constructor(traitGroup, options){
635817
const {
@@ -643,11 +825,14 @@ class ModelTrait{
643825
textureCollection,
644826
blendshapeTraits,
645827
colorCollection,
828+
decalCollection,
829+
decalMeshNameTargets,
646830
fullDirectory,
647831
fullThumbnail,
648832
}= options;
649833
this.manifestData = traitGroup.manifestData;
650834
this.traitGroup = traitGroup;
835+
this.decalMeshNameTargets = getAsArray(decalMeshNameTargets);
651836

652837
this.id = id;
653838
this.directory = directory;
@@ -683,6 +868,7 @@ class ModelTrait{
683868

684869
this.targetTextureCollection = textureCollection ? traitGroup.manifestData.getTextureGroup(textureCollection) : null;
685870
this.targetColorCollection = colorCollection ? traitGroup.manifestData.getColorGroup(colorCollection) : null;
871+
this.targetDecalCollection = decalCollection ? traitGroup.manifestData.getDecalGroup(decalCollection) : null;
686872

687873
if(blendshapeTraits && Array.isArray(blendshapeTraits)){
688874

@@ -838,6 +1024,10 @@ export class BlendShapeTrait{
8381024
}
8391025

8401026
class TextureTrait{
1027+
/**
1028+
* @param {TraitTexturesGroup} traitGroup
1029+
* @param {TextureCollectionItem} options
1030+
*/
8411031
constructor(traitGroup, options){
8421032
const {
8431033
id,
@@ -872,6 +1062,67 @@ class TextureTrait{
8721062
this.fullThumbnail = traitGroup.manifestData.getThumbnailsDirectory() + thumbnail;
8731063
}
8741064
}
1065+
export class DecalTrait extends TextureTrait{
1066+
/**
1067+
* @type {string}
1068+
*/
1069+
id
1070+
/**
1071+
* @type {string}
1072+
*/
1073+
directory
1074+
/**
1075+
* @type {string | undefined}
1076+
* */
1077+
fullDirectory
1078+
name
1079+
thumbnail
1080+
/**
1081+
* @type {string|undefined}
1082+
*/
1083+
fullThumbnail
1084+
/**
1085+
* @type {TraitTexturesGroup}
1086+
*/
1087+
traitGroup
1088+
/**
1089+
* @param {TraitTexturesGroup} traitGroup
1090+
* @param {TextureCollectionItem} options
1091+
*/
1092+
constructor( traitGroup, options){
1093+
super(traitGroup,options);
1094+
const {
1095+
id,
1096+
directory,
1097+
fullDirectory,
1098+
name,
1099+
thumbnail,
1100+
}= options;
1101+
this.traitGroup = traitGroup;
1102+
this.id = id;
1103+
this.directory = directory;
1104+
if (fullDirectory){
1105+
this.fullDirectory = fullDirectory
1106+
}
1107+
else{
1108+
if (Array.isArray(directory))
1109+
{
1110+
this.fullDirectory = [];
1111+
for (let i =0;i< directory.length;i++){
1112+
this.fullDirectory[i] = traitGroup.manifestData.getTraitsDirectory() + directory[i]
1113+
}
1114+
}
1115+
else
1116+
{
1117+
this.fullDirectory = traitGroup.manifestData.getTraitsDirectory() + thumbnail;
1118+
}
1119+
}
1120+
1121+
this.name = name;
1122+
this.thumbnail = thumbnail;
1123+
this.fullThumbnail = traitGroup.manifestData.getThumbnailsDirectory() + thumbnail;
1124+
}
1125+
}
8751126
class ColorTrait{
8761127
constructor(traitGroup, options){
8771128
const {

‎src/library/OverlayTextureManager.js

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
2+
/** @typedef {import("@pixiv/three-vrm").VRM} VRM */
3+
import * as THREE from "three";
4+
/** @typedef {import("./CharacterManifestData").TraitModelsGroup} TraitModelsGroup */
5+
/** @typedef {import("./characterManager").CharacterManager} CharacterManager */
6+
import { combineURLs } from "./load-utils";
7+
import { createContext, getAsArray } from "./utils";
8+
import TextureImageDataRenderer from "./textureImageDataRenderer";
9+
10+
/**
11+
* Implementation of a Decal-like system.
12+
* Will overlay textures on top of the base texture of a VRM mesh and through a renderer will bake the textures into a single texture.
13+
* When textures are added or removed, the renderer will update the texture on the VRM mesh.
14+
*
15+
* Not the best implementation, but it works.
16+
*/
17+
export default class OverlayedTextureManager{
18+
/**
19+
* @type {THREE.SkinnedMesh<THREE.BufferGeometry, THREE.MeshStandardMaterial>[]}
20+
*/
21+
targetVRMMeshes = [];
22+
/**
23+
* The base texture of the VRM mesh
24+
* @type {THREE.Texture|null}
25+
*/
26+
baseTexture = null;
27+
/**
28+
* @type {THREE.Texture[]}
29+
*/
30+
textures = [];
31+
/**
32+
* @type {Map<string, THREE.Texture>}
33+
*/
34+
applied = new Map();
35+
/**
36+
* @type {TextureImageDataRenderer}
37+
*/
38+
imageDataRenderer;
39+
/**
40+
*
41+
* @param {CharacterManager} characterManager
42+
*/
43+
constructor(characterManager){
44+
this.characterManager = characterManager;
45+
// create a renderer to render the textures; size is arbitrary (replaced later)
46+
this.imageDataRenderer = new TextureImageDataRenderer(512, 512);
47+
}
48+
49+
get scene(){
50+
return this.characterManager.parentModel
51+
}
52+
53+
get manifest(){
54+
return this.characterManager.manifestData;
55+
}
56+
57+
/**
58+
* Make sure the target material has a diffuse map
59+
*/
60+
get targetMaterial(){
61+
if(!this.targetVRMMeshes.length){
62+
throw new Error("No target meshes found, call setTargetVRM");
63+
}
64+
65+
let mats = this.targetVRMMeshes.map(mesh=>getAsArray(mesh.material)).flat();
66+
let mat = mats[0]
67+
if(!mat.map){
68+
for(let i = 1; i < mats.length; i++){
69+
if(!mats[i].map){
70+
continue;
71+
}else{
72+
mat = mats[i];
73+
break;
74+
}
75+
}
76+
return mat
77+
}else{
78+
return mat;
79+
}
80+
81+
}
82+
83+
/**
84+
* @param {VRM} targetVRM - the VRM to apply the overlay textures to
85+
* @param {string[]} [decalMeshNameTargets] - [optional] a list of mesh names to target for decal application; if not provided, all skinned meshes will be targeted
86+
*/
87+
setTargetVRM(targetVRM, decalMeshNameTargets){
88+
this.targetVRMMeshes = [];
89+
targetVRM.scene.traverse((child)=>{
90+
if (child instanceof THREE.SkinnedMesh){
91+
if(decalMeshNameTargets && decalMeshNameTargets.length){
92+
// if we've defined a list of mesh names to target, only add those meshes
93+
if(decalMeshNameTargets.includes(child.name)){
94+
this.targetVRMMeshes.push(child);
95+
}
96+
}else{
97+
this.targetVRMMeshes.push(child);
98+
}
99+
}
100+
})
101+
}
102+
103+
async update(){
104+
const mat = this.targetMaterial
105+
const image = mat.map.image
106+
const width = image.width;
107+
const height = image.height;
108+
// clear imageDataRenderer
109+
this.imageDataRenderer.clearRenderer();
110+
this.imageDataRenderer.width = width;
111+
this.imageDataRenderer.height = height;
112+
113+
// render the textures through the imageDataRenderer
114+
const imgData= this.imageDataRenderer.render(this.textures, mat.color || new THREE.Color(1, 1, 1), new THREE.Color(1, 1, 1), true, true);
115+
if(!imgData){
116+
console.error("Failed to update OverlayTextureManager, ImageData is undefined");
117+
}
118+
const context = createContext({width,height,transparent:true})
119+
120+
const bitmap = await createImageBitmap(imgData)
121+
context.drawImage(bitmap,0,0)
122+
const texture = new THREE.Texture(context.canvas)
123+
texture.colorSpace = THREE.SRGBColorSpace;
124+
// flip the texture
125+
texture.flipY = false;
126+
texture.needsUpdate = true;
127+
128+
// update the material
129+
this.targetMaterial.map = texture
130+
}
131+
132+
/**
133+
* Add an overlay texture to the VRM mesh
134+
* @param {TraitModelsGroup} traitGroup trait group to load the decal from
135+
* @param {string} decalId decal ID to load
136+
* @returns
137+
*/
138+
async loadOverlayTexture(traitGroup,decalId){
139+
const textureLoader = new THREE.TextureLoader();
140+
const decal = traitGroup.getAllDecals().find(decal=>decal.id === decalId);
141+
if(!decal) {
142+
throw new Error("Decal "+decalId+" not found in trait group");
143+
}
144+
if(this.targetVRMMeshes.length === 0){
145+
throw new Error("No target meshes found");
146+
}
147+
const diffusePath = decal.directory;
148+
if(!diffusePath) {
149+
throw new Error("Decal not found in trait group");
150+
}
151+
152+
const diffuseFullPath = combineURLs(this.manifest.getTraitsDirectory(),diffusePath);
153+
const decalDiffuse = await textureLoader.loadAsync( diffuseFullPath );
154+
155+
decalDiffuse.colorSpace = THREE.SRGBColorSpace;
156+
decalDiffuse.flipY = false;
157+
if(!this.textures.length){
158+
this.textures.push(this.targetMaterial.map.clone());
159+
}
160+
this.textures.push(decalDiffuse);
161+
this.applied.set(decalId,decalDiffuse);
162+
163+
return this.update();
164+
}
165+
166+
/**
167+
* @param {string} decalId
168+
* @returns
169+
*/
170+
removeOverlayTexture( decalId ){
171+
if(this.applied.has(decalId)){
172+
const text = this.applied.get(decalId);
173+
if(!text) {
174+
this.applied.delete(decalId);
175+
return Promise.resolve();
176+
}
177+
this.textures.splice(this.textures.indexOf(text),1);
178+
this.applied.delete(decalId);
179+
}
180+
return this.update()
181+
}
182+
183+
184+
removeAllOverlayedTextures(){
185+
this.textures = [this.textures[0]];
186+
this.applied.clear();
187+
this.update();
188+
}
189+
}

‎src/library/characterManager.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getNodesWithColliders, saveVRMCollidersToUserData, renameMorphTargets}
1111
import { cullHiddenMeshes, setTextureToChildMeshes, addChildAtFirst } from "./utils";
1212
import { LipSync } from "./lipsync";
1313
import { LookAtManager } from "./lookatManager";
14+
import OverlayedTextureManager from "./OverlayTextureManager";
1415
import { CharacterManifestData } from "./CharacterManifestData";
1516

1617
const mouse = new THREE.Vector2();
@@ -45,6 +46,7 @@ export class CharacterManager {
4546
this.lookAtManager = null;
4647
this.animationManager = new AnimationManager();
4748
this.screenshotManager = new ScreenshotManager(this, parentModel || this.rootModel);
49+
this.overlayedTextureManager = new OverlayedTextureManager(this)
4850
this.blinkManager = new BlinkManager(0.1, 0.1, 0.5, 5)
4951

5052

@@ -1464,8 +1466,13 @@ export class CharacterManager {
14641466
this._displayModel(vrm)
14651467

14661468
this._applyManagers(vrm)
1469+
1470+
if(this.overlayedTextureManager){
1471+
if(traitModel.targetDecalCollection){
1472+
this.overlayedTextureManager.setTargetVRM(vrm, traitModel.decalMeshNameTargets)
1473+
}
1474+
}
14671475

1468-
console.log(this.characterModel)
14691476
// and then add the new avatar data
14701477
// to do, we are now able to load multiple vrm models per options, set the options to include vrm arrays
14711478
this.avatar[traitGroupID] = {

‎src/library/create-texture-atlas.js

+3-13
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,10 @@ import * as THREE from "three";
22
import { mergeGeometry } from "./merge-geometry.js";
33
import { MToonMaterial } from "@pixiv/three-vrm";
44
import squaresplit from 'squaresplit';
5+
import {createContext} from "./utils.js";
56
import TextureImageDataRenderer from "./textureImageDataRenderer.js";
67

7-
function createContext({ width, height, transparent }) {
8-
const canvas = document.createElement("canvas");
9-
canvas.width = width;
10-
canvas.height = height;
11-
const context = canvas.getContext("2d");
12-
context.fillStyle = "white";
13-
if (transparent)
14-
context.globalAlpha = 0;
15-
context.fillRect(0, 0, canvas.width, canvas.height);
16-
context.globalAlpha = 1;
17-
return context;
18-
}
8+
199
function getTextureImage(material, textureName) {
2010

2111
// material can come in arrays or single values, in case of ccoming in array take the first one
@@ -439,7 +429,7 @@ export const createTextureAtlasBrowser = async ({ backColor, includeNonTexturedM
439429
if (usesNormal == false && name == 'normal' && texture != null){
440430
usesNormal = true;
441431
}
442-
const imgData = textureImageDataRenderer.render(texture, multiplyColor, clearColor, ATLAS_SIZE_PX, ATLAS_SIZE_PX, name == 'diffuse' && transparentTexture, name != 'normal');
432+
const imgData = textureImageDataRenderer.render(texture, multiplyColor, clearColor, name == 'diffuse' && transparentTexture, name != 'normal');
443433
createImageBitmap(imgData)// bmp is trasnaprent
444434
.then((bmp) => context.drawImage(bmp, min.x * ATLAS_SIZE_PX, min.y * ATLAS_SIZE_PX, xTileSize, yTileSize));
445435
});

‎src/library/load-utils.js

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import { getAsArray, renameVRMBones,getUniqueId } from "../library/utils"
44
import { findChildByName } from '../library/utils';
55
import { PropertyBinding,SkinnedMesh } from 'three';
66

7+
/**
8+
* @param {string} baseURL base of a path or URL
9+
* @param {string} relativeURL next path
10+
* @returns
11+
*/
12+
export function combineURLs(baseURL, relativeURL) {
13+
return relativeURL && baseURL
14+
? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
15+
: baseURL ? baseURL: relativeURL;
16+
}
17+
718
export const loadVRM = async(url) => {
819
const gltfLoader = new GLTFLoader()
920
gltfLoader.crossOrigin = 'anonymous';

‎src/library/textureImageDataRenderer.js

+99-34
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,115 @@
11
import * as THREE from 'three'
2-
2+
import { getAsArray } from './utils'
33
export default class TextureImageDataRenderer {
44
width
55
height
66
cameraRTT = null
77
sceneRTT = null
8-
material = null
9-
quad = null
8+
materials = []
9+
quads = []
1010
renderer = null
1111
rtTexture = null
1212
constructor(width, height) {
1313
this.width = width
1414
this.height = height
1515
}
1616

17-
render(texture, multiplyColor, clearColor, width, height, isTransparent,sRGBEncoding = true) {
17+
/**
18+
*
19+
* @param {THREE.Texture} texture
20+
* @param {number} positionIndex
21+
* @returns
22+
*/
23+
_addPlane(texture,positionIndex=0) {
24+
if(!this.sceneRTT){
25+
return
26+
}
27+
const material = new THREE.MeshBasicMaterial({
28+
side: THREE.DoubleSide,
29+
transparent: true,
30+
opacity: 1,
31+
color: new THREE.Color(1, 1, 1),
32+
})
33+
this.materials.push(material)
34+
const plane = new THREE.PlaneGeometry(1, 1)
35+
const m = new THREE.Mesh(plane, material)
36+
m.position.z = positionIndex*0.0001
37+
m.scale.set(this.width, this.height, 1)
38+
this.quads.push(m)
39+
this.sceneRTT.add(m)
40+
}
41+
42+
render(texture, multiplyColor, clearColor, isTransparent,sRGBEncoding = true) {
1843
// if texture is null or undefined, create a texture only with clearColor (that is color type)
19-
if (!texture) {
20-
texture = TextureImageDataRenderer.createSolidColorTexture(clearColor, width, height)
44+
const textures = getAsArray(texture)
45+
if (textures.length === 0) {
46+
texture = getAsArray(TextureImageDataRenderer.createSolidColorTexture(clearColor, this.width, this.height))
2147
}
2248

2349
if (this.renderer == null) {
2450
this.sceneRTT = new THREE.Scene()
25-
this.cameraRTT = new THREE.OrthographicCamera(-width / 2, width / 2, height / 2, -height / 2, -10000, 10000)
51+
this.cameraRTT = new THREE.OrthographicCamera(-this.width / 2, this.width / 2, this.height / 2, -this.height / 2, -10000, 10000)
2652
this.cameraRTT.position.z = 100
2753

2854
this.sceneRTT.add(this.cameraRTT)
2955

30-
this.material = new THREE.MeshBasicMaterial({
31-
side: THREE.DoubleSide,
32-
transparent: true,
33-
opacity: 1,
34-
color: new THREE.Color(1, 1, 1),
35-
})
36-
37-
const plane = new THREE.PlaneGeometry(1, 1)
38-
this.quad = new THREE.Mesh(plane, this.material)
39-
this.quad.scale.set(width, height, 1)
40-
this.sceneRTT.add(this.quad)
56+
for(let i = 0; i < textures.length; i++){
57+
const textureElement = textures[i]
58+
if(textureElement){
59+
this._addPlane(textureElement, i)
60+
}
61+
}
4162

4263
this.renderer = new THREE.WebGLRenderer()
4364
this.renderer.setPixelRatio(1)
44-
this.renderer.setSize(width, height)
65+
this.renderer.setSize(this.width, this.height)
4566
//renderer.setClearColor(new Color(1, 1, 1), 1);
4667
this.renderer.autoClear = false
4768
} else {
4869
if(this.cameraRTT){
49-
this.cameraRTT.left = -width / 2
50-
this.cameraRTT.right = width / 2
51-
this.cameraRTT.top = height / 2
52-
this.cameraRTT.bottom = -height / 2
70+
this.cameraRTT.left = -this.width / 2
71+
this.cameraRTT.right = this.width / 2
72+
this.cameraRTT.top = this.height / 2
73+
this.cameraRTT.bottom = -this.height / 2
5374

5475
this.cameraRTT.updateProjectionMatrix()
5576
}
5677

57-
this.quad?.scale.set(width, height, 1)
78+
this.quads.forEach((quad) => {
79+
quad?.scale.set(this.width, this.height, 1)
80+
})
5881

59-
this.renderer.setSize(width, height)
82+
this.renderer.setSize(this.width, this.height)
6083
}
6184

62-
this.rtTexture = new THREE.WebGLRenderTarget(width, height)
63-
this.rtTexture.texture.colorSpace = sRGBEncoding ? THREE.SRGBColorSpace : THREE.NoColorSpace;
85+
/**
86+
* If the number of textures is greater than the number of materials, add a plane for each texture
87+
*/
88+
if(textures.length > this.materials.length){
89+
const diff = textures.length - this.materials.length
90+
for(let i = 0; i < diff; i++){
91+
const textureElement = textures[i]
92+
this._addPlane(textureElement, i)
93+
}
94+
}else{
95+
this.materials.length = textures.length
96+
if(this.quads.length > textures.length){
97+
for(let i = textures.length; i < this.quads.length; i++){
98+
this.sceneRTT?.remove(this.quads[i])
99+
}
100+
}
101+
this.quads.length = textures.length
102+
}
64103

65-
if(this.material){
66-
this.material.map = texture
67-
this.material.color = multiplyColor.clone()
104+
this.rtTexture = new THREE.WebGLRenderTarget(this.width, this.height)
105+
this.rtTexture.texture.colorSpace = sRGBEncoding ? THREE.SRGBColorSpace : THREE.NoColorSpace;
68106

107+
for(let i = 0; i < textures.length; i++){
108+
const textureElement = textures[i]
109+
if(textureElement){
110+
this.materials[i].map = textureElement
111+
this.materials[i].color = multiplyColor.clone()
112+
}
69113
}
70114
// set opacoty to 0 if texture is transparent
71115
this.renderer.setClearColor(clearColor.clone(), isTransparent ? 0 : 1)
@@ -77,18 +121,39 @@ export default class TextureImageDataRenderer {
77121
}
78122

79123
let buffer = new Uint8ClampedArray(this.rtTexture.width * this.rtTexture.height * 4)
80-
this.renderer.readRenderTargetPixels(this.rtTexture, 0, 0, width, height, buffer)
81-
const imgData = new ImageData(buffer, width, height)
124+
this.renderer.readRenderTargetPixels(this.rtTexture, 0, 0, this.width, this.height, buffer)
125+
const imgData = new ImageData(buffer, this.width, this.height)
82126

83127
return imgData
128+
}
129+
130+
131+
clearRenderer() {
132+
this.rtTexture?.dispose()
133+
this.rtTexture = null
134+
if(this.materials?.length){
135+
this.materials.forEach((material) => {
136+
material.map?.dispose()
137+
material.map = null
138+
})
139+
}
84140
}
85141

86142
destroy() {
87143
this.cameraRTT = null
88144
this.sceneRTT?.clear()
89145
this.sceneRTT = null
90-
this.material = null
91-
this.quad = null
146+
147+
this.materials.forEach((material) => {
148+
material.map?.dispose()
149+
material.map = null
150+
})
151+
this.quads.forEach((quad) => {
152+
this.sceneRTT?.remove(quad)
153+
quad = null
154+
})
155+
this.materials.length = 0
156+
this.quads.length = 0
92157
this.renderer?.dispose()
93158
this.renderer = null
94159
this.rtTexture = null

‎src/library/utils.js

+13-25
Original file line numberDiff line numberDiff line change
@@ -298,31 +298,6 @@ async function getScreenShotByElementId(id) {
298298
return blob;
299299
});
300300
}
301-
function createSpecifiedImage(ctx){
302-
const context = createContext(256,256);
303-
const imageData = ctx.getImageData(left, top, width, height);
304-
const arr = new ImageData(imageData, xTileSize, yTileSize);
305-
const tempcanvas = document.createElement("canvas");
306-
tempcanvas.width = xTileSize;
307-
tempcanvas.height = yTileSize;
308-
const tempctx = tempcanvas.getContext("2d");
309-
310-
tempctx.putImageData(arr, 0, 0);
311-
tempctx.save();
312-
// draw tempctx onto context
313-
context.drawImage(tempcanvas, min.x * ATLAS_SIZE_PX, min.y * ATLAS_SIZE_PX, xTileSize, yTileSize);
314-
315-
}
316-
317-
function createContext({ width, height }) {
318-
const canvas = document.createElement("canvas");
319-
canvas.width = width;
320-
canvas.height = height;
321-
const context = canvas.getContext("2d");
322-
context.fillStyle = "white";
323-
context.fillRect(0, 0, canvas.width, canvas.height);
324-
return context;
325-
}
326301

327302
export async function getSkinColor(scene, targets) {
328303
for (const target of targets) {
@@ -873,3 +848,16 @@ export function doesMeshHaveMorphTargetBoundToManager(mesh, oldDictionary){
873848
}
874849
return false
875850
}
851+
852+
export function createContext({ width, height, transparent }) {
853+
const canvas = document.createElement("canvas");
854+
canvas.width = width;
855+
canvas.height = height;
856+
const context = canvas.getContext("2d");
857+
context.fillStyle = "white";
858+
if (transparent)
859+
context.globalAlpha = 0;
860+
context.fillRect(0, 0, canvas.width, canvas.height);
861+
context.globalAlpha = 1;
862+
return context;
863+
}

‎src/pages/Appearance.jsx

+59-40
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import React, { useContext, useEffect } from "react"
1+
import React, { useContext } from "react"
22
import styles from "./Appearance.module.css"
3-
import { Color } from "three"
43
import { ViewMode, ViewContext } from "../context/ViewContext"
54
import { SceneContext } from "../context/SceneContext"
65
import CustomButton from "../components/custom-button"
@@ -11,18 +10,25 @@ import FileDropComponent from "../components/FileDropComponent"
1110
import { getFileNameWithoutExtension } from "../library/utils"
1211
import MenuTitle from "../components/MenuTitle"
1312
import BottomDisplayMenu from "../components/BottomDisplayMenu"
14-
import {BlendShapeTrait} from '../library/CharacterManifestData'
13+
import decalPicker from "../images/sticker.png"
1514
import { TokenBox } from "../components/token-box/TokenBox"
1615
import JsonAttributes from "../components/JsonAttributes"
1716
import cancel from "../images/cancel.png"
17+
import DecalGridView from "../components/decals/decalGrid"
1818
import randomizeIcon from "../images/randomize.png"
1919
import colorPicker from "../images/color-palette.png"
2020
import { ChromePicker } from 'react-color'
2121
import RightPanel from "../components/RightPanel"
2222

23+
/**
24+
* @typedef {import("../library/CharacterManifestData.js").TraitModelsGroup} TraitModelsGroup
25+
* @typedef {import("../library/CharacterManifestData.js").ModelTrait} ModelTrait
26+
*/
27+
2328
export const TraitPage ={
2429
TRAIT:0,
2530
BLEND_SHAPE:1,
31+
DECAL:2
2632
}
2733

2834
function Appearance() {
@@ -32,10 +38,6 @@ function Appearance() {
3238
characterManager,
3339
animationManager,
3440
moveCamera,
35-
loraDataGenerator,
36-
spriteAtlasGenerator,
37-
thumbnailsGenerator,
38-
sceneElements
3941
} = React.useContext(SceneContext)
4042

4143
const [traitView, setTraitView] = React.useState(TraitPage.TRAIT)
@@ -55,7 +57,13 @@ function Appearance() {
5557

5658
const [jsonSelectionArray, setJsonSelectionArray] = React.useState(null)
5759
const [traits, setTraits] = React.useState(null)
58-
const [traitGroupName, setTraitGroupName] = React.useState("")
60+
/**
61+
* @type {[TraitModelsGroup, React.Dispatch<TraitModelsGroup>]} state
62+
*/
63+
const [selectedTraitGroup, setSelectedTraitGroup] = React.useState(null)
64+
/**
65+
* @type {[ModelTrait|null, React.Dispatch<ModelTrait|null>]} state
66+
*/
5967
const [selectedTrait, setSelectedTrait] = React.useState(null)
6068
const [selectedBlendshapeTraits, setSelectedBlendshapeTraits] = React.useState({})
6169
const [selectedVRM, setSelectedVRM] = React.useState(null)
@@ -74,8 +82,8 @@ function Appearance() {
7482
setJsonSelectionArray(null);
7583
characterManager.loadRandomTraits().then(() => {
7684
console.log("success")
77-
if (traitGroupName != ""){
78-
setSelectedTrait(characterManager.getCurrentTraitData(traitGroupName));
85+
if (selectedTraitGroup && selectedTraitGroup.trait != ""){
86+
setSelectedTrait(characterManager.getCurrentTraitData(selectedTraitGroup.trait));
7987
}
8088
setIsLoading(false);
8189
})
@@ -90,7 +98,7 @@ function Appearance() {
9098
}
9199
const handleChangeComplete = (color) =>{
92100
setColorPicked({ background: color.hex });
93-
characterManager.setTraitColor(traitGroupName, color.hex);
101+
characterManager.setTraitColor(selectedTraitGroup?.trait, color.hex);
94102
}
95103

96104
const handleAnimationDrop = async (file) => {
@@ -102,10 +110,10 @@ function Appearance() {
102110

103111
const handleImageDrop = (file) => {
104112
setIsPickingColor(false);
105-
if (traitGroupName != ""){
113+
if (selectedTraitGroup && selectedTraitGroup.trait != ""){
106114
setIsLoading(true);
107115
const path = URL.createObjectURL(file);
108-
characterManager.loadCustomTexture(traitGroupName, path).then(()=>{
116+
characterManager.loadCustomTexture(selectedTraitGroup.trait, path).then(()=>{
109117
setIsLoading(false);
110118
})
111119
}
@@ -115,10 +123,10 @@ function Appearance() {
115123
}
116124
const handleVRMDrop = (file) =>{
117125
setIsPickingColor(false);
118-
if (traitGroupName != ""){
126+
if (selectedTraitGroup && selectedTraitGroup.trait != ""){
119127
setIsLoading(true);
120128
const path = URL.createObjectURL(file);
121-
characterManager.loadCustomTrait(traitGroupName, path).then(()=>{
129+
characterManager.loadCustomTrait(selectedTraitGroup.trait, path).then(()=>{
122130
setIsLoading(false);
123131
})
124132
}
@@ -229,10 +237,10 @@ function Appearance() {
229237
const selectTraitGroup = (traitGroup) => {
230238
!isMute && playSound('optionClick');
231239
setIsPickingColor(false);
232-
if (traitGroupName !== traitGroup.trait){
240+
if (selectedTraitGroup?.trait !== traitGroup.trait){
233241
setTraitView(TraitPage.TRAIT);
234242
setTraits(characterManager.getTraits(traitGroup.trait));
235-
setTraitGroupName(traitGroup.trait);
243+
setSelectedTraitGroup(traitGroup);
236244

237245
const selectedT = characterManager.getCurrentTraitData(traitGroup.trait)
238246
const selectedBlendshapeTraits = characterManager.getCurrentBlendShapeTraitData(traitGroup.trait);
@@ -245,7 +253,7 @@ function Appearance() {
245253
}
246254
else{
247255
setTraits(null);
248-
setTraitGroupName("");
256+
setSelectedTraitGroup(null)
249257
setSelectedTrait(null);
250258
setSelectedBlendshapeTraits({})
251259
moveCamera({ targetY: 0.8, distance: 3.2 })
@@ -258,13 +266,15 @@ function Appearance() {
258266
var input = document.createElement('input');
259267
input.type = 'file';
260268
input.accept=".vrm"
261-
269+
if(!selectedTraitGroup){
270+
return console.error("Please select a trait group first")
271+
}
262272
input.onchange = e => {
263273
var file = e.target.files[0];
264274
if (file.name.endsWith(".vrm")){
265275
const url = URL.createObjectURL(file);
266276
setIsLoading(true);
267-
characterManager.loadCustomTrait(traitGroupName,url).then(()=>{
277+
characterManager.loadCustomTrait(selectedTraitGroup.trait,url).then(()=>{
268278
setIsLoading(false);
269279
})
270280
}
@@ -297,7 +307,7 @@ function Appearance() {
297307
<TokenBox
298308
size={56}
299309
icon={ traitGroup.fullIconSvg }
300-
rarity={traitGroupName !== traitGroup.trait ? "none" : "mythic"}
310+
rarity={selectedTraitGroup?.trait !== traitGroup.trait ? "none" : "mythic"}
301311

302312
/>
303313
<div className={styles["editorText"]}>{traitGroup.name}</div>
@@ -310,32 +320,38 @@ function Appearance() {
310320
</div>
311321

312322
{/* Option Selection section */
313-
!!traits && (
323+
!!traits && selectedTraitGroup && (
314324
<div className={styles["selectorContainerPos"]}>
315325

316-
<MenuTitle title={traitGroupName} width={130} left={20}/>
317-
318-
{/* color section */
319-
selectedTrait && traitView==TraitPage.TRAIT &&(
320-
<div className={styles["selectorColorPickerButton"]}
321-
onClick={()=>{setIsPickingColor(!isPickingColor)}}
322-
>
323-
<img className={styles["selectorColorPickerImg"]} src={colorPicker}/>
324-
</div>
325-
)}
326-
{
326+
<MenuTitle title={selectedTraitGroup.trait} width={130} left={20}/>
327+
<div
328+
className={styles["selectorPickerTabs"]}
329+
>
330+
{/* color section */
331+
selectedTrait && traitView==TraitPage.TRAIT && (
332+
<div className={styles["selectorColorPickerButton"]}
333+
onClick={()=>{setIsPickingColor(!isPickingColor)}}
334+
>
335+
<img className={styles["selectorColorPickerImg"]} src={colorPicker}/>
336+
</div>
337+
)}
338+
{selectedTraitGroup && selectedTraitGroup.getAllDecals()?.length && <div className={styles["selectorColorPickerButton"]}
339+
onClick={()=>traitView==TraitPage.DECAL?setTraitView(TraitPage.TRAIT):setTraitView(TraitPage.DECAL)}
340+
>
341+
<img className={styles["selectorColorPickerImg"]} src={decalPicker}/>
342+
</div>}
343+
</div>
344+
{
327345
traitView==TraitPage.TRAIT && !!isPickingColor && (<div
328346
draggable = {false}
329347
className={styles["selectorColorPickerUI"]}>
330348
<ChromePicker
331-
draggable = {false}
332-
width={'200px'}
349+
styles={{ default: {picker:{ width: '200px' }} }}
333350
color={ colorPicked.background }
334351
onChange={ handleColorChange }
335352
onChangeComplete={ handleChangeComplete }
336353
/>
337354
</div>)}
338-
339355
<div className={styles["bottomLine"]} />
340356
<div className={styles["scrollContainerOptions"]}>
341357
{traitView == TraitPage.TRAIT && (
@@ -344,7 +360,7 @@ function Appearance() {
344360
<div
345361
key={"randomize-trait"}
346362
className={`${styles["selectorButton"]}`}
347-
onClick={() => {randomTrait(traitGroupName)}}
363+
onClick={() => {randomTrait(selectedTraitGroup.trait)}}
348364
>
349365
<TokenBox
350366
size={56}
@@ -354,12 +370,12 @@ function Appearance() {
354370
</div>
355371
}
356372
{/* Null button section */
357-
!characterManager.isTraitGroupRequired(traitGroupName) ? (
373+
!characterManager.isTraitGroupRequired(selectedTraitGroup.trait) ? (
358374
<div
359375
key={"no-trait"}
360376
className={`${styles["selectorButton"]}`}
361377
icon={cancel}
362-
onClick={() => {removeTrait(traitGroupName)}}
378+
onClick={() => {removeTrait(selectedTraitGroup.trait)}}
363379
>
364380
<TokenBox
365381
size={56}
@@ -392,6 +408,9 @@ function Appearance() {
392408
{traitView == TraitPage.BLEND_SHAPE && (
393409
<BlendShapeTraitView selectedTrait={selectedTrait} onBack={()=>{setTraitView(TraitPage.TRAIT)}} selectedBlendShapeTrait={selectedBlendshapeTraits} setSelectedBlendshapeTrait={setSelectedBlendshapeTraits} />
394410
)}
411+
{traitView == TraitPage.DECAL && (
412+
<DecalGridView selectedTraitGroup={selectedTraitGroup} onBack={()=>{setTraitView(TraitPage.TRAIT)}} />
413+
)}
395414
</div>
396415

397416
<div className={styles["uploadContainer"]}>
@@ -407,7 +426,7 @@ function Appearance() {
407426
)}
408427
<JsonAttributes jsonSelectionArray={jsonSelectionArray}/>
409428

410-
<RightPanel selectedTrait={selectedTrait} selectedVRM={selectedVRM} traitGroupName={traitGroupName}/>
429+
<RightPanel selectedTrait={selectedTrait} selectedVRM={selectedVRM} traitGroupName={selectedTraitGroup?.trait||""}/>
411430

412431
<BottomDisplayMenu loadedAnimationName={loadedAnimationName} randomize={randomize}/>
413432
<div className={styles.buttonContainer}>

‎src/pages/Appearance.module.css

+8-3
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,19 @@
154154
background: rgba(5, 11, 14, 0.8);
155155
width:40px;
156156
height: 40px;
157-
position: absolute;
158-
right: -40px;
159-
top: 15px;
160157
display: flex;
161158
align-items: center;
162159
border-radius: 0 8px 8px 0;
163160
cursor: pointer;
164161
}
162+
.selectorPickerTabs {
163+
position: absolute;
164+
right: -40px;
165+
top: 15px;
166+
display: flex;
167+
flex-direction: column;
168+
gap:2px;
169+
}
165170
.selectorColorPickerImg{
166171
width:20px;
167172
height: 20px;

‎src/sticker.png

448 Bytes
Loading

0 commit comments

Comments
 (0)
Please sign in to comment.