Loading...
const shopifySettings = {
sceneBackgroundUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/Scene_BAckground.jpg?v=1743082591", // Default or from setting
cssVariables: {
"--page-background-color": "#111111",
"--description-bg-color": "rgba(20, 20, 20, 0.6)",
"--description-text-color": "#eeeeee",
"--shop-button-bg-color": "#555555",
"--shop-button-text-color": "#ffffff",
"--arrow-color": "#dddddd",
"--loading-text-color": "#cccccc"
},
selectedModels: [
{ baseModelId: 0, customTextures: { shirt: null, pants: null } }, // Leto Original
{ baseModelId: 1, customTextures: { shirt: null, pants: null } }, // Reyna Original
{ baseModelId: 2, customTextures: { shirt: null, pants: null } }, // Aurora Original
// Add more models based on theme settings here, e.g.:
// { baseModelId: 3, customTextures: { shirt: "URL_FROM_SHOPIFY_SETTING_1", pants: null } },
]
};
// --- End Placeholder ---
// Global variables
var scene, camera, renderer, controls;
var textureLoader = new THREE.TextureLoader();
var gltfLoader = new THREE.GLTFLoader();
var currentModelGroup = null;
var currentIndex = 0;
var baseModelRef = null; // Reference to the loaded base model for positioning
// --- Base Model Definitions ---
const baseModelDefinitions = [
// 0: Leto (Original)
{
id: 0, // Unique ID for reference
url: "https://cdn.shopify.com/3d/models/a3aafcc308f26bf3/REALTHINKLeto.glb",
description: "GRAPHIC TEE AND BAGGY PANTS",
shirtUrl: "https://cdn.shopify.com/3d/models/127271fcb01312af/SHIRT4.glb",
pantsUrl: "https://cdn.shopify.com/3d/models/0625595fbfa16e12/NEWPANTS.glb",
shoesUrl: "https://cdn.shopify.com/3d/models/c0b3038cdecf35ea/NEWSHOES.glb",
// Default Textures (used if not overridden by customTextures)
skinTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/Skin_diffuse.jpg?v=1743094864",
hairTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/Leon_Hair_D.png?v=1743274464",
shirtTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/MDESIGNS_SHIRT_LOGO.jpg?v=1743096076",
pantsTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/Wide_Leg_1.jpg?v=1743097739",
eyeTextureUrl: null, eyelashTextureUrl: null, hairUrl: null, glassesUrl: null
},
// 1: Reyna (Original - Sweatpants)
{
id: 1,
url: "https://cdn.shopify.com/3d/models/bb046ad3a1ad6e97/UNDERWEARReyna.glb",
description: "HOODIE AND SWEATPANTS",
shirtUrl: "https://cdn.shopify.com/3d/models/17b450cd0910f060/OPT_HOODIE.glb",
pantsUrl: "https://cdn.shopify.com/3d/models/bf3c398c679cc2d5/SUBPANTS.glb",
shoesUrl: "https://cdn.shopify.com/3d/models/7a422b0f6afebee2/OPT_SHOES.glb",
skinTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/AV06_Skin_Diffuse_ffe00b65-117f-4522-8b84-d7de46040da9.jpg?v=1743300188",
hairTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/pl0070_hair_atos.texout.png?v=1743274452",
shirtTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/OFFICIAL_HOODIE.jpg?v=1743373101",
pantsTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/ACT.OFFICAL_SWEATPANTS_LAYOUT.jpg?v=1743373881",
eyeTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/AV03_Eyes.jpg?v=1743375316",
eyelashTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/Avatar_06_Eyelashes_a679b085-5e06-4c4b-a14e-47f744fb1427.jpg?v=1743375423",
hairUrl: null, glassesUrl: null
},
// 2: Aurora (Original - Shorts)
{
id: 2,
url: "https://cdn.shopify.com/3d/models/349a35a75a6dffe3/NOAurora.glb",
description: "ZIPUP AND SHORTS",
hairUrl: "https://cdn.shopify.com/3d/models/662721de03975b23/HAIRAurora.glb",
glassesUrl: "https://cdn.shopify.com/3d/models/f28410c04a1b0b63/GLASSESAurora.glb",
shirtUrl: "https://cdn.shopify.com/3d/models/8fb153f7173131df/ZIPUPAurora.glb",
pantsUrl: "https://cdn.shopify.com/3d/models/d703b3b0423af8ca/SHORTSAurora.glb",
shoesUrl: "https://cdn.shopify.com/3d/models/10326e578ded1db6/SHOESAurora.glb",
skinTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/abstract-smooth-brown-wall-background-layout-design-web-template-business-report-with-smooth-circle-gradient-color.jpg?v=1743552098",
hairTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/AV03_Hair.png?v=1743550860",
shirtTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/ZIPUP_LAYOUT.jpg?v=1743551626",
pantsTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/SHORT_POCEKTS.jpg?v=1743551607",
eyeTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/AV03_Eyes_6fbfea7e-849b-444b-85bd-8db182306954.jpg?v=1743551823",
eyelashTextureUrl: null
},
// 3: Leto (Sweatpants)
{
id: 3,
url: "https://cdn.shopify.com/3d/models/a3aafcc308f26bf3/REALTHINKLeto.glb",
description: "GRAPHIC TEE WITH SWEATPANTS",
shirtUrl: "https://cdn.shopify.com/3d/models/127271fcb01312af/SHIRT4.glb",
pantsUrl: "https://cdn.shopify.com/3d/models/35c46b11d8af6a4a/NEWPANTS2nd.glb",
shoesUrl: "https://cdn.shopify.com/3d/models/c0b3038cdecf35ea/NEWSHOES.glb",
skinTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/Skin_diffuse.jpg?v=1743094864",
hairTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/Leon_Hair_D.png?v=1743274464",
shirtTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/MDESIGNS_SHIRT_LOGO.jpg?v=1743096076",
pantsTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/ACT.OFFICAL_SWEATPANTS_LAYOUT.jpg?v=1743373881",
eyeTextureUrl: null, eyelashTextureUrl: null, hairUrl: null, glassesUrl: null
},
// 4: Reyna (Baggy Pants)
{
id: 4,
url: "https://cdn.shopify.com/3d/models/bb046ad3a1ad6e97/UNDERWEARReyna.glb",
description: "HOODIE AND BAGGY PANTS",
shirtUrl: "https://cdn.shopify.com/3d/models/17b450cd0910f060/OPT_HOODIE.glb",
pantsUrl: "https://cdn.shopify.com/3d/models/66926559f8aa7c01/FINALPANTSReyna.glb",
shoesUrl: "https://cdn.shopify.com/3d/models/7a422b0f6afebee2/OPT_SHOES.glb",
skinTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/AV06_Skin_Diffuse_ffe00b65-117f-4522-8b84-d7de46040da9.jpg?v=1743300188",
hairTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/pl0070_hair_atos.texout.png?v=1743274452",
shirtTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/OFFICIAL_HOODIE.jpg?v=1743373101",
pantsTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/Wide_Leg_1.jpg?v=1743097739",
eyeTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/AV03_Eyes.jpg?v=1743375316",
eyelashTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/Avatar_06_Eyelashes_a679b085-5e06-4c4b-a14e-47f744fb1427.jpg?v=1743375423",
hairUrl: null, glassesUrl: null
},
// 5: Aurora (Baggy Pants)
{
id: 5,
url: "https://cdn.shopify.com/3d/models/349a35a75a6dffe3/NOAurora.glb",
description: "ZIPUP AND BAGGY PANTS",
hairUrl: "https://cdn.shopify.com/3d/models/662721de03975b23/HAIRAurora.glb",
glassesUrl: "https://cdn.shopify.com/3d/models/f28410c04a1b0b63/GLASSESAurora.glb",
shirtUrl: "https://cdn.shopify.com/3d/models/8fb153f7173131df/ZIPUPAurora.glb",
pantsUrl: "https://cdn.shopify.com/3d/models/63232fdc1b7342fa/BAGGYPANTSAurora.glb",
shoesUrl: "https://cdn.shopify.com/3d/models/10326e578ded1db6/SHOESAurora.glb",
skinTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/abstract-smooth-brown-wall-background-layout-design-web-template-business-report-with-smooth-circle-gradient-color.jpg?v=1743552098",
hairTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/AV03_Hair.png?v=1743550860",
shirtTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/ZIPUP_LAYOUT.jpg?v=1743551626",
pantsTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/Wide_Leg_1.jpg?v=1743097739",
eyeTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/AV03_Eyes_6fbfea7e-849b-444b-85bd-8db182306954.jpg?v=1743551823",
eyelashTextureUrl: null
},
// 6: Aurora (Sweatpants)
{
id: 6,
url: "https://cdn.shopify.com/3d/models/349a35a75a6dffe3/NOAurora.glb",
description: "ZIPUP AND SWEATPANTS",
hairUrl: "https://cdn.shopify.com/3d/models/662721de03975b23/HAIRAurora.glb",
glassesUrl: "https://cdn.shopify.com/3d/models/f28410c04a1b0b63/GLASSESAurora.glb",
shirtUrl: "https://cdn.shopify.com/3d/models/8fb153f7173131df/ZIPUPAurora.glb",
pantsUrl: "https://cdn.shopify.com/3d/models/97f4524843063215/SWEATPANTSOFFAurora.glb",
shoesUrl: "https://cdn.shopify.com/3d/models/10326e578ded1db6/SHOESAurora.glb",
skinTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/abstract-smooth-brown-wall-background-layout-design-web-template-business-report-with-smooth-circle-gradient-color.jpg?v=1743552098",
hairTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/AV03_Hair.png?v=1743550860",
shirtTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/ZIPUP_LAYOUT.jpg?v=1743551626",
pantsTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/ACT.OFFICAL_SWEATPANTS_LAYOUT.jpg?v=1743373881",
eyeTextureUrl: "https://cdn.shopify.com/s/files/1/0934/1144/2979/files/AV03_Eyes_6fbfea7e-849b-444b-85bd-8db182306954.jpg?v=1743551823",
eyelashTextureUrl: null
}
];
// --- The actual list of models to display, derived from settings ---
const modelsToDisplay = shopifySettings.selectedModels.map(selected => {
const baseDefinition = baseModelDefinitions.find(def => def.id === selected.baseModelId);
if (!baseDefinition) {
console.warn(`Base model definition not found for ID: ${selected.baseModelId}`);
return null;
}
return {
...baseDefinition,
shirtTextureUrl: selected.customTextures?.shirt || baseDefinition.shirtTextureUrl,
pantsTextureUrl: selected.customTextures?.pants || baseDefinition.pantsTextureUrl,
};
}).filter(model => model !== null);
// --- Texture Loading Cache ---
const textureCache = {};
function loadTextureCached(url, name) {
if (!url) return Promise.resolve(null);
if (textureCache[url]) {
return Promise.resolve(textureCache[url]);
}
// console.log(`Loading texture: ${name} (${url})`);
return new Promise((resolve, reject) => {
textureLoader.load(
url,
(texture) => {
// console.log(`Texture loaded: ${name}`);
texture.flipY = false;
texture.encoding = THREE.sRGBEncoding;
textureCache[url] = texture;
resolve(texture);
},
undefined,
(err) => {
console.error(`Error loading texture ${name} (${url}):`, err);
reject(err);
}
);
});
}
function applyCssVariables(variables) {
const root = document.documentElement;
for (const [key, value] of Object.entries(variables)) {
if (key.startsWith('--')) {
root.style.setProperty(key, value);
}
}
}
function init() {
if (shopifySettings.cssVariables) {
applyCssVariables(shopifySettings.cssVariables);
}
scene = new THREE.Scene();
const backgroundUrl = shopifySettings.sceneBackgroundUrl || getComputedStyle(document.documentElement).getPropertyValue('--scene-background-image').replace(/url\(['"]?(.*?)['"]?\)/i, '$1');
if (backgroundUrl) {
textureLoader.load(backgroundUrl, (texture) => {
texture.encoding = THREE.sRGBEncoding;
scene.background = texture;
console.log("Scene background loaded from:", backgroundUrl);
}, undefined, (err) => {
console.error("Error loading scene background:", err);
scene.background = new THREE.Color(getComputedStyle(document.documentElement).getPropertyValue('--page-background-color') || '#000000');
});
} else {
scene.background = new THREE.Color(getComputedStyle(document.documentElement).getPropertyValue('--page-background-color') || '#000000');
}
camera = new THREE.PerspectiveCamera(
50,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 1, 3);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.physicallyCorrectLights = true;
document.getElementById("scene-container").appendChild(renderer.domElement);
var ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
var directionalLight = new THREE.DirectionalLight(0xffffff, 2.0);
directionalLight.position.set(3, 3, 3);
scene.add(directionalLight);
var directionalLight2 = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight2.position.set(-3, 2, -3);
scene.add(directionalLight2);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.minDistance = 1.5;
controls.maxDistance = 6;
controls.target.set(0, 0.9, 0);
controls.update();
if (modelsToDisplay.length > 0) {
loadOutfit(currentIndex);
} else {
console.warn("No models selected or configured to display.");
document.getElementById("description").innerText = "No models selected";
document.getElementById("loading-indicator").style.display = "none";
}
window.addEventListener("resize", onWindowResize);
animate();
}
function loadGLB(url, partName) {
return new Promise((resolve, reject) => {
if (!url) {
// console.log(`Skipping load for ${partName} (no URL)`);
resolve(null);
return;
}
// console.log(`Loading ${partName}: ${url}`);
gltfLoader.load(
url,
(gltf) => {
// console.log(`${partName} loaded successfully.`);
resolve(gltf.scene);
},
undefined,
(error) => {
console.error(`Error loading ${partName}: ${url}`, error);
reject(error);
}
);
});
}
function positionAndTextureItem(itemModel, texturePromise, baseModel, options = {}) {
const { offset = { x: 0, y: 0, z: 0 }, rotationX = 0 } = options;
if (!baseModel) {
console.error("Base model reference missing for positioning item:", itemModel?.name);
return Promise.reject("Base model missing");
}
itemModel.scale.copy(baseModel.scale);
itemModel.position.copy(baseModel.position);
itemModel.position.x += offset.x;
itemModel.position.y += offset.y;
itemModel.position.z += offset.z;
itemModel.rotation.set(rotationX, 0, 0);
return texturePromise.then(texture => {
if (texture) {
applyTextureToObject(itemModel, texture);
} else {
// console.log(`No texture provided or loaded for item: ${itemModel.name || 'Unnamed'}. Skipping texture application.`);
}
return itemModel;
}).catch(err => {
console.error(`Failed to apply texture to ${itemModel.name || 'Unnamed'}:`, err);
return itemModel;
});
}
function applyTextureToObject(object, texture) {
if (!object || !texture) return;
object.traverse(function (child) {
if (child.isMesh && child.material) {
let materialToClone = Array.isArray(child.material) ? child.material[0] : child.material;
if (materialToClone) {
let newMaterial = materialToClone.clone();
newMaterial.map = texture;
if (newMaterial.map) newMaterial.map.encoding = THREE.sRGBEncoding;
const nameLower = child.name.toLowerCase();
const matNameLower = materialToClone.name?.toLowerCase();
if (nameLower.includes('hair') || matNameLower?.includes('hair') || nameLower.includes('eyelash') || matNameLower?.includes('eyelash')) {
newMaterial.transparent = true;
newMaterial.alphaTest = 0.5;
newMaterial.depthWrite = false;
newMaterial.side = THREE.DoubleSide;
}
newMaterial.needsUpdate = true;
child.material = newMaterial;
}
}
});
}
async function loadOutfit(index) {
const modelData = modelsToDisplay[index];
if (!modelData) {
console.error("No model data found for index:", index, "in modelsToDisplay list.");
document.getElementById("description").innerText = "Error: Model not found";
document.getElementById("loading-indicator").style.display = "none";
return;
}
console.log("Starting loadOutfit for index:", index, "Model ID:", modelData.id);
document.getElementById("loading-indicator").style.display = "block";
document.getElementById("description").innerText = "Loading...";
if (currentModelGroup) {
scene.remove(currentModelGroup);
currentModelGroup.traverse(function(object) {
if (object.geometry) object.geometry.dispose();
if (object.material) {
const disposeMaterial = (mat) => {
if (mat && mat.map && !textureCache[mat.map.image?.src]) { // Only dispose non-cached maps
mat.map.dispose();
}
if (mat) mat.dispose();
};
if (Array.isArray(object.material)) object.material.forEach(disposeMaterial);
else disposeMaterial(object.material);
}
});
// console.log("Previous model group removed and resources disposed/managed.");
}
currentModelGroup = new THREE.Group();
scene.add(currentModelGroup);
baseModelRef = null;
try {
const baseModelPromise = loadGLB(modelData.url, `Base Model (ID: ${modelData.id})`);
const shirtModelPromise = loadGLB(modelData.shirtUrl, `Shirt (ID: ${modelData.id})`);
const pantsModelPromise = loadGLB(modelData.pantsUrl, `Pants (ID: ${modelData.id})`);
const shoesModelPromise = loadGLB(modelData.shoesUrl, `Shoes (ID: ${modelData.id})`);
const hairModelPromise = modelData.hairUrl ? loadGLB(modelData.hairUrl, `Hair (ID: ${modelData.id})`) : Promise.resolve(null);
const glassesModelPromise = modelData.glassesUrl ? loadGLB(modelData.glassesUrl, `Glasses (ID: ${modelData.id})`) : Promise.resolve(null);
const skinTexturePromise = loadTextureCached(modelData.skinTextureUrl, `Skin Texture (ID: ${modelData.id})`);
const hairTexturePromise = loadTextureCached(modelData.hairTextureUrl, `Hair Texture (ID: ${modelData.id})`);
const shirtTexturePromise = loadTextureCached(modelData.shirtTextureUrl, `Shirt Texture (ID: ${modelData.id})`);
const pantsTexturePromise = loadTextureCached(modelData.pantsTextureUrl, `Pants Texture (ID: ${modelData.id})`);
const eyeTexturePromise = loadTextureCached(modelData.eyeTextureUrl, `Eye Texture (ID: ${modelData.id})`);
const eyelashTexturePromise = loadTextureCached(modelData.eyelashTextureUrl, `Eyelash Texture (ID: ${modelData.id})`);
const baseModel = await baseModelPromise;
if (!baseModel) throw new Error("Base model failed to load.");
baseModelRef = baseModel;
baseModel.scale.set(1.2, 1.2, 1.2);
baseModel.position.set(0, -0.9, 0);
const isAuroraVariant = modelData.id === 2 || modelData.id === 5 || modelData.id === 6;
const isLetoVariant = modelData.id === 0 || modelData.id === 3;
if (isLetoVariant || isAuroraVariant) baseModel.rotation.x = -Math.PI / 2;
else baseModel.rotation.set(0, 0, 0);
currentModelGroup.add(baseModel);
const [skinTexture, baseHairTexture, eyeTexture, eyelashTexture] = await Promise.all([
skinTexturePromise, hairTexturePromise, eyeTexturePromise, eyelashTexturePromise
]);
baseModel.traverse(function (child) {
if (child.isMesh) {
const nameLower = child.name.toLowerCase();
const matNameLower = child.material?.name?.toLowerCase();
let textureToApply = null;
if (nameLower.includes('skin') || nameLower.includes('body')) textureToApply = skinTexture;
else if (!isAuroraVariant && (nameLower.includes('hair') || matNameLower?.includes('hair'))) {
textureToApply = baseHairTexture;
if (isLetoVariant) child.rotation.x = Math.PI / 2;
}
else if (!isLetoVariant && (nameLower.includes('eye') || matNameLower?.includes('eye')) && !nameLower.includes('eyelash')) textureToApply = eyeTexture;
else if (!isLetoVariant && !isAuroraVariant && (nameLower.includes('eyelash') || matNameLower?.includes('eyelash'))) textureToApply = eyelashTexture;
if (textureToApply) applyTextureToObject(child, textureToApply);
}
});
// console.log("Base model processed.");
const [shirtModel, pantsModel, shoesModel, hairModel, glassesModel, shirtTexture, pantsTexture, auroraHairTexture] = await Promise.all([
shirtModelPromise, pantsModelPromise, shoesModelPromise, hairModelPromise, glassesModelPromise,
shirtTexturePromise, pantsTexturePromise,
isAuroraVariant ? hairTexturePromise : Promise.resolve(null)
]);
const processingPromises = [];
if (shirtModel) processingPromises.push(positionAndTextureItem(shirtModel, Promise.resolve(shirtTexture), baseModelRef, { rotationX: (isLetoVariant) ? -Math.PI / 2 : 0 }).then(m => currentModelGroup.add(m)));
if (pantsModel) processingPromises.push(positionAndTextureItem(pantsModel, Promise.resolve(pantsTexture), baseModelRef).then(m => currentModelGroup.add(m)));
if (shoesModel) processingPromises.push(positionAndTextureItem(shoesModel, Promise.resolve(null), baseModelRef).then(m => currentModelGroup.add(m)));
if (hairModel) processingPromises.push(positionAndTextureItem(hairModel, Promise.resolve(auroraHairTexture), baseModelRef).then(m => currentModelGroup.add(m)));
if (glassesModel) processingPromises.push(positionAndTextureItem(glassesModel, Promise.resolve(null), baseModelRef).then(m => currentModelGroup.add(m)));
await Promise.all(processingPromises);
console.log("All parts loaded and processed for outfit index:", index);
document.getElementById("description").innerText = modelData.description;
} catch (error) {
console.error("Error loading outfit:", error);
document.getElementById("description").innerText = "Error loading model";
if (currentModelGroup) scene.remove(currentModelGroup);
currentModelGroup = null;
} finally {
document.getElementById("loading-indicator").style.display = "none";
}
}
function prevModel() {
if (modelsToDisplay.length <= 1) return;
currentIndex = (currentIndex - 1 + modelsToDisplay.length) % modelsToDisplay.length;
loadOutfit(currentIndex);
}
function nextModel() {
if (modelsToDisplay.length <= 1) return;
currentIndex = (currentIndex + 1) % modelsToDisplay.length;
loadOutfit(currentIndex);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
// --- Start Initialization ---
if (modelsToDisplay.length > 0) {
init();
} else {
console.warn("Initialization skipped: No models configured in shopifySettings.selectedModels.");
document.getElementById("description").innerText = "No models to display.";
if (shopifySettings.cssVariables) {
applyCssVariables(shopifySettings.cssVariables);
}
}