Skip to content

Commit 6ff88e3

Browse files
authored
Multi model example (#5169)
1 parent da46f5f commit 6ff88e3

4 files changed

Lines changed: 316 additions & 1 deletion

File tree

packages/modelviewer.dev/data/examples.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@
285285
}
286286
]
287287
},
288+
{
289+
"name": "Multiple Models",
290+
"htmlName": "multimodel",
291+
"examples": [
292+
{
293+
"htmlId": "multimodel-actions",
294+
"name": "Multiple Models Actions"
295+
}
296+
]
297+
},
288298
{
289299
"name": "Post Process Effects",
290300
"htmlName": "postprocessing",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
18+
<!DOCTYPE html>
19+
<html lang="en">
20+
<head>
21+
<meta charset="utf-8">
22+
<meta http-equiv="refresh" content="0; url='./multimodel/index.html'" />
23+
</head>
24+
<body>
25+
<p>Please follow <a href="./multimodel/index.html">this link</a>.</p>
26+
</body>
27+
</html>
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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>&lt;model-viewer&gt; Multiple Models</title>
22+
<meta charset="utf-8">
23+
<meta name="description" content="&lt;model-viewer&gt; 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 &lt;model-viewer&gt; canvas.
54+
</h4>
55+
<div class="heading">
56+
<h2 class="demo-title">Interact with Multiple Models</h2>
57+
<p>
58+
By placing declarative <code>&lt;extra-model&gt;</code> elements inside
59+
<code>&lt;model-viewer&gt;</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>

packages/modelviewer.dev/src/docs-and-examples/sidebar.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ const identicalCategories = [
7070
'annotations',
7171
'lightingandenv',
7272
'animation',
73-
'scenegraph'
73+
'scenegraph',
74+
'multimodel'
7475
];
7576

7677
export function getSidebarCategoryForNewPage(): string {

0 commit comments

Comments
 (0)