|
| 1 | +<!-- |
| 2 | +/* @license |
| 3 | + * Copyright 2026 Google Inc. All Rights Reserved. |
| 4 | + * Licensed under the Apache License, Version 2.0 (the 'License'); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an 'AS IS' BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | +--> |
| 17 | +<!DOCTYPE html> |
| 18 | +<html lang="en"> |
| 19 | + |
| 20 | +<head> |
| 21 | + <title><model-viewer> Multiple Models</title> |
| 22 | + <meta charset="utf-8"> |
| 23 | + <meta name="description" content="<model-viewer> multiple models examples"> |
| 24 | + <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 25 | + <link type="text/css" href="../../styles/examples.css" rel="stylesheet" /> |
| 26 | + <link type="text/css" href="../../styles/docs.css" rel="stylesheet" /> |
| 27 | + <link rel="shortcut icon" type="image/png" href="../../assets/favicon.png" /> |
| 28 | + |
| 29 | + <script defer src="https://web3dsurvey.com/collector.js"></script> |
| 30 | + <script> |
| 31 | + window.ga = window.ga || function () { (ga.q = ga.q || []).push(arguments) }; ga.l = +new Date; |
| 32 | + ga('create', 'UA-169901325-1', { 'storage': 'none' }); |
| 33 | + ga('set', 'referrer', document.referrer.split('?')[0]); |
| 34 | + ga('set', 'anonymizeIp', true); |
| 35 | + ga('send', 'pageview'); |
| 36 | + </script> |
| 37 | + <script async src='https://www.google-analytics.com/analytics.js'></script> |
| 38 | +</head> |
| 39 | + |
| 40 | +<body> |
| 41 | + |
| 42 | + <div class="examples-page"> |
| 43 | + <div class="sidebar" id="sidenav"></div> |
| 44 | + <div id="toggle"></div> |
| 45 | + |
| 46 | + <div class="examples-container"> |
| 47 | + <div class="sample"> |
| 48 | + <div id="multimodel-actions" class="demo"></div> |
| 49 | + <div class="content"> |
| 50 | + <div class="wrapper"> |
| 51 | + <h4 id="intro"> |
| 52 | + <span class="font-medium">Multiple Models.</span> |
| 53 | + Load and control multiple 3D models within a single <model-viewer> canvas. |
| 54 | + </h4> |
| 55 | + <div class="heading"> |
| 56 | + <h2 class="demo-title">Interact with Multiple Models</h2> |
| 57 | + <p> |
| 58 | + By placing declarative <code><extra-model></code> elements inside |
| 59 | + <code><model-viewer></code>, you can load multiple separate 3D models into a single scene. |
| 60 | + Each model has its own position offset, orientation, and scale, and can be manipulated |
| 61 | + individually. |
| 62 | + </p> |
| 63 | + <p> |
| 64 | + This example displays three models, each showcasing a different type of interaction: |
| 65 | + </p> |
| 66 | + <ul> |
| 67 | + <li><strong>Sphere (Base Model):</strong> Changing material base color factor dynamically.</li> |
| 68 | + <li><strong>Robot (Extra Model 1):</strong> Targeting animation play/pause to a specific model index.</li> |
| 69 | + <li><strong>Astronaut (Extra Model 2):</strong> Animating the position offset of a model over time.</li> |
| 70 | + </ul> |
| 71 | + </div> |
| 72 | + <example-snippet stamp-to="multimodel-actions" highlight-as="html"> |
| 73 | + <template> |
| 74 | + <style> |
| 75 | + .controls { |
| 76 | + position: absolute; |
| 77 | + bottom: 20px; |
| 78 | + left: 20px; |
| 79 | + right: 20px; |
| 80 | + display: flex; |
| 81 | + justify-content: space-around; |
| 82 | + align-items: center; |
| 83 | + padding: 12px; |
| 84 | + z-index: 10; |
| 85 | + } |
| 86 | + .control-group { |
| 87 | + display: flex; |
| 88 | + flex-direction: column; |
| 89 | + align-items: center; |
| 90 | + text-align: center; |
| 91 | + } |
| 92 | + .control-group h4 { |
| 93 | + margin: 0 0 8px 0; |
| 94 | + font-size: 0.9em; |
| 95 | + font-weight: 500; |
| 96 | + color: #202124; |
| 97 | + } |
| 98 | + .control-group button { |
| 99 | + font-size: 0.9em; |
| 100 | + padding: 8px 16px; |
| 101 | + margin: 0 !important; |
| 102 | + background-color: #ffffffd0; |
| 103 | + border: 1px solid #dadce0; |
| 104 | + border-radius: 4px; |
| 105 | + box-shadow: 0 1px 2px rgba(0,0,0,0.05); |
| 106 | + cursor: pointer; |
| 107 | + font-weight: 500; |
| 108 | + transition: background-color 0.2s, transform 0.1s, box-shadow 0.2s; |
| 109 | + } |
| 110 | + .control-group button:hover { |
| 111 | + background-color: #ffffff; |
| 112 | + box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
| 113 | + transform: translateY(-1px); |
| 114 | + } |
| 115 | + .control-group button:active { |
| 116 | + transform: translateY(0); |
| 117 | + } |
| 118 | + .control-group button:disabled { |
| 119 | + opacity: 0.5; |
| 120 | + cursor: not-allowed; |
| 121 | + transform: none; |
| 122 | + } |
| 123 | + @media (max-width: 600px) { |
| 124 | + .controls { |
| 125 | + flex-direction: column; |
| 126 | + bottom: 10px; |
| 127 | + left: 10px; |
| 128 | + right: 10px; |
| 129 | + gap: 12px; |
| 130 | + } |
| 131 | + } |
| 132 | + </style> |
| 133 | + |
| 134 | + <model-viewer id="multiModel" camera-controls touch-action="pan-y" |
| 135 | + src="../../shared-assets/models/reflective-sphere.gltf" |
| 136 | + scale="0.5 0.5 0.5" shadow-intensity="1" alt="Multiple Models Demonstration"> |
| 137 | + |
| 138 | + <extra-model id="robot" |
| 139 | + src="../../shared-assets/models/RobotExpressive.glb" |
| 140 | + offset="-2.0 0 0" scale="0.2 0.2 0.2" orientation="0 30 0"> |
| 141 | + </extra-model> |
| 142 | + |
| 143 | + <extra-model id="astronaut" |
| 144 | + src="../../shared-assets/models/Astronaut.glb" |
| 145 | + offset="2.0 0 0" scale="0.3 0.3 0.3" orientation="0 -30 0"> |
| 146 | + </extra-model> |
| 147 | + |
| 148 | + <div class="controls glass"> |
| 149 | + <div class="control-group"> |
| 150 | + <h4>Base Model (Sphere)</h4> |
| 151 | + <button id="color-btn">Random Color</button> |
| 152 | + </div> |
| 153 | + <div class="control-group"> |
| 154 | + <h4>Model 1 (Robot)</h4> |
| 155 | + <button id="animate-btn">Play Animation</button> |
| 156 | + </div> |
| 157 | + <div class="control-group"> |
| 158 | + <h4>Model 2 (Astronaut)</h4> |
| 159 | + <button id="jump-btn">Jump</button> |
| 160 | + </div> |
| 161 | + </div> |
| 162 | + </model-viewer> |
| 163 | + <script type="module"> |
| 164 | + const modelViewer = document.querySelector("#multiModel"); |
| 165 | + const colorBtn = document.querySelector("#color-btn"); |
| 166 | + const animateBtn = document.querySelector("#animate-btn"); |
| 167 | + const jumpBtn = document.querySelector("#jump-btn"); |
| 168 | + const astronautExtra = document.querySelector("#astronaut"); |
| 169 | + |
| 170 | + // 1. Base Model (Sphere): Change color factor randomly |
| 171 | + colorBtn.addEventListener("click", () => { |
| 172 | + if (modelViewer.model && modelViewer.model.materials.length > 0) { |
| 173 | + const [material] = modelViewer.model.materials; |
| 174 | + const hue = Math.floor(Math.random() * 360); |
| 175 | + const randomColor = `hsl(${hue}, 90%, 60%)`; |
| 176 | + material.pbrMetallicRoughness.setBaseColorFactor(randomColor); |
| 177 | + } |
| 178 | + }); |
| 179 | + |
| 180 | + // 2. Robot (Model index 1): Toggle Play/Pause animation |
| 181 | + let robotPlaying = false; |
| 182 | + animateBtn.addEventListener("click", () => { |
| 183 | + if (modelViewer.availableAnimations.length > 0) { |
| 184 | + if (robotPlaying) { |
| 185 | + modelViewer.pause({modelIndex: 1}); |
| 186 | + animateBtn.textContent = "Play Animation"; |
| 187 | + robotPlaying = false; |
| 188 | + } else { |
| 189 | + modelViewer.animationName = "Dance"; |
| 190 | + modelViewer.play({modelIndex: 1}); |
| 191 | + animateBtn.textContent = "Pause Animation"; |
| 192 | + robotPlaying = true; |
| 193 | + } |
| 194 | + } |
| 195 | + }); |
| 196 | + |
| 197 | + // 3. Astronaut (Model index 2): Float/Jump animation using offset attribute |
| 198 | + let jumping = false; |
| 199 | + jumpBtn.addEventListener("click", () => { |
| 200 | + if (jumping) return; |
| 201 | + jumping = true; |
| 202 | + jumpBtn.disabled = true; |
| 203 | + |
| 204 | + let startTime = null; |
| 205 | + const duration = 1000; // 1 second jump duration |
| 206 | + const jumpHeight = 0.8; // jump height in meters |
| 207 | + |
| 208 | + const animateJump = (timestamp) => { |
| 209 | + if (!startTime) startTime = timestamp; |
| 210 | + const elapsed = timestamp - startTime; |
| 211 | + const progress = elapsed / duration; |
| 212 | + |
| 213 | + if (progress < 1) { |
| 214 | + // Sine wave interpolation |
| 215 | + const y = Math.sin(progress * Math.PI) * jumpHeight; |
| 216 | + astronautExtra.setAttribute("offset", `2.0 ${y} 0`); |
| 217 | + requestAnimationFrame(animateJump); |
| 218 | + } else { |
| 219 | + astronautExtra.setAttribute("offset", "2.0 0 0"); |
| 220 | + jumpBtn.disabled = false; |
| 221 | + jumping = false; |
| 222 | + } |
| 223 | + }; |
| 224 | + requestAnimationFrame(animateJump); |
| 225 | + }); |
| 226 | + |
| 227 | + modelViewer.addEventListener("load", () => { |
| 228 | + // Pre-configure animation name for the robot |
| 229 | + modelViewer.animationName = "Dance"; |
| 230 | + // Frame all 3 models in the view |
| 231 | + modelViewer.updateFraming(); |
| 232 | + }); |
| 233 | + </script> |
| 234 | + </template> |
| 235 | + </example-snippet> |
| 236 | + </div> |
| 237 | + </div> |
| 238 | + </div> |
| 239 | + |
| 240 | + <div class="footer"> |
| 241 | + <ul> |
| 242 | + <li class="attribution"> |
| 243 | + <a href="https://github.com/mrdoob/three.js/tree/dev/examples/models/gltf/RobotExpressive">RobotExpressive</a> |
| 244 | + by <a href="https://www.patreon.com/quaternius">Tomás Laulhé</a>, |
| 245 | + licensed under <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>. |
| 246 | + </li> |
| 247 | + <li class="attribution"> |
| 248 | + <a href="https://poly.google.com/view/dLHpzNdygsg">Astronaut</a> by <a |
| 249 | + href="https://poly.google.com/user/4aEd8rQgKu2">Poly</a>, |
| 250 | + licensed under <a href="https://creativecommons.org/licenses/by/2.0/">CC-BY</a>. |
| 251 | + </li> |
| 252 | + </ul> |
| 253 | + <div style="margin-top:24px;" class="copyright">©Copyright 2018-2026 Google Inc. Licensed under the Apache |
| 254 | + License 2.0.</div> |
| 255 | + <div id='footer-links'></div> |
| 256 | + </div> |
| 257 | + </div> |
| 258 | + </div> |
| 259 | + |
| 260 | + <script type="module" src="../../examples/built/docs-and-examples.js"> |
| 261 | + </script> |
| 262 | + <script type="module"> |
| 263 | + (() => { init('examples-multimodel'); })(); |
| 264 | + (() => { initFooterLinks(); })(); |
| 265 | + </script> |
| 266 | + |
| 267 | + <!-- Documentation-specific dependencies: --> |
| 268 | + <script type="module" src="../built/dependencies.js"> |
| 269 | + </script> |
| 270 | + |
| 271 | + <!-- Loads <model-viewer> on modern browsers: --> |
| 272 | + <script type="module" src="../../../../node_modules/@google/model-viewer/dist/model-viewer.js"> |
| 273 | + </script> |
| 274 | + |
| 275 | +</body> |
| 276 | + |
| 277 | +</html> |
0 commit comments