Loading...
Shop Now
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); } }