Эта статья является четвертой в серии статей для начинающих о создании 3D-сцен с помощью svelte-cubed и three.js. Если вы хотите узнать, как мы пришли к этому, вы можете начать с самого начала:
Часть первая: Svelte-Cubed: Введение в 3D в браузере
Часть вторая: Svelte-Cubed: Добавление движения в 3D-сцены
Часть третья: Svelte-Cubed: Создание доступного и последовательного опыта на разных устройствах
Октаэдры — это прекрасно, но во многих случаях вам понадобится более сложная модель, созданная в таком инструменте моделирования, как Blender. Эти модели могут быть представлены в различных типах файлов (например, glTF, fbx и т.д.). Мы будем работать с файлами gltf
, но этот подход подойдет и для других.
Проще говоря, мы используем обычный threejs GLTFLoader
, а затем передаем модель компоненту svelte cubed. Все просто. Но если вы, как и я, новичок в 3D, пошаговое руководство будет полезным, и мы разобьем его на несколько небольших шагов, чтобы показать модель, а затем некоторые дополнительные шаблоны, которые вы можете реализовать для повторного использования и загрузки.
Если вы знаете, что делаете, и просто хотите перейти к коду, переходите к финальной сцене: https://svelte.dev/repl/8ea0488302bb434991cc5b82f653cdb5?version=3.48.0.
- Что мы рассмотрим:
- Шаг 1: Найдите классные модели
- Примеры моделей Threejs
- Группа Khronos
- SketchFab
- Шаг 2: Создайте компонент Svelte для модели
- Шаг 3: Импортируйте GLTFLoader из threejs
- Шаг 4: Загрузка модели при монтировании
- Шаг 5: Условный рендеринг модели
- Дополнительно: Многократно используемый компонент GLTF
- Дополнительно: Обработка состояния загрузки
- Ресурсы
Что мы рассмотрим:
- Поиск крутых моделей
- Создание компонента Svelte для модели
- Импортируйте GLTFLoader из threejs
- Загрузите модель
onMount
. - Условно передайте ее в компонент svelte cubed
<Primative />
.
Дополнительно:
- Создать многократно используемый GLTF-компонент
- Обработка состояний загрузки
Шаг 1: Найдите классные модели
Примеры моделей Threejs
Вы можете найти кучу примеров моделей glTF/glb из самого Threejs в руководстве или в каталоге примеров
Группа Khronos
Группа Khronos отвечает за спецификацию glTF. У них также есть каталог примеров
SketchFab
На SketchFab есть огромный выбор моделей, многие из которых вы можете скачать бесплатно. И, конечно, пожалуйста, отдавайте должное создателям моделей.
Шаг 2: Создайте компонент Svelte для модели
Используйте этот REPL, чтобы начать с базовой сцены с кубиками Svelte: https://svelte.dev/repl/c0c34349f1f6405bb25700599a841083?version=3.48.0.
Добавьте новый компонент под названием LittleCity.svelte
с тегом script добавьте две переменные:
- URL к файлу glTF
- Пустая переменная, которая будет содержать модель, когда мы ее получим.
<script>
/*
Model taken from the threejs examples. Created by antonmoek:
https://sketchfab.com/antonmoek
*/
const modelURL = 'https://threejs.org/manual/examples/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf'
const model = null;
</script>
Шаг 3: Импортируйте GLTFLoader из threejs
Какой бы тип файла у вас ни был, в threejs, вероятно, есть загрузчик для него. В нашем случае (или при использовании glb) мы будем использовать GLTFLoader. Добавьте этот импорт в верхнюю часть тега вашего скрипта и ОБЯЗАТЕЛЬНО ВКЛЮЧИТЕ РАСШИРЕНИЕ ФАЙЛА.
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
Шаг 4: Загрузка модели при монтировании
Когда наш компонент монтируется, мы хотим асинхронно загрузить модель и присвоить ее переменной model
.
Сначала мы создадим функцию load, которая инстанцирует загрузчик и вернет обещание с нашей моделью, а затем вызовем эту функцию onMount
и обработаем ответ (который и будет моделью).
import { onMount } from 'svelte';
// …
function loadGLTF() {
const loader = new GLTFLoader();
return loader.loadAsync(modelURL)
}
onMount(() => {
loadGLTF().then(_model => model = _model);
})
Вот и все! Мы добавим блок catch позже, когда сделаем этот компонент многоразового использования. Если вы никогда раньше не работали с моделями, обязательно выполните консольный лог модели, чтобы посмотреть, как выглядит эта структура данных! Там есть много интересных и (потенциально) полезных артефактов.
Шаг 5: Условный рендеринг модели
Svelte Cubed предоставляет компонент под названием Primitive
для работы практически со всем, что не является сеткой (например, модели, оси, сетка и т.д.). Primitive может принимать реквизиты, такие как позиция, вращение, масштаб и другие ожидаемые параметры. Реквизит object
примитива будет принимать scene
данных нашей модели. Естественно, мы хотим отобразить его только в том случае, если у нас загружена модель, поэтому мы обернем его в условный оператор.
<script>
import * as SC from 'svelte-cubed';
// …
</script>
{#if model}
<SC.Primitive
object={model.scene}
scale={[.05,.05,.05]}
/>
{/if}
В App.svelte
импортируем и закинем наш компонент:
<script>
// … other imports
import LittleCity from './LittleCity.svelte';
// …
</script>
<SC.Canvas>
<!-- … all the scene stuff is here -->
<LittleCity />
</SC.Canvas>
ВАУ! Посмотрите на этот город!
Это основной подход для загрузки отдельной модели, но это много кода, который нужно переписывать каждый раз, когда вы хотите загрузить одну модель. Кроме того, было бы неплохо указывать статус загрузки моделей. Поэтому в следующем разделе мы создадим базовый многократно используемый компонент GLTF и обработаем загрузку.
Вкратце, вот что мы имеем на данный момент в нашем компоненте LittleCity.svelte
:
<script>
import * as SC from 'svelte-cubed';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { onMount } from 'svelte';
import { modelURL } from './stores';
const modelURL = 'https://threejs.org/manual/examples/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf'
let model = null;
function loadGLTF() {
const loader = new GLTFLoader();
return loader.loadAsync(modelURL['littleCity']);
}
onMount(() => {
loadGLTF().then(_model => model = _model);
})
</script>
{#if model}
<SC.Primitive
object={model.scene}
scale={[.05,.05,.05]}
/>
{/if}
Дополнительно: Многократно используемый компонент GLTF
Это базовая реализация. Поделитесь своими улучшениями в комментариях!
Сначала мы настроим некоторые глобальные состояния в новом файле stores.js
, чтобы помочь нам управлять URL-адресами моделей и обрабатывать состояния загрузки.
import { writable, readable, derived } from 'svelte/store';
// Contains the status of all models
export const statusOfModels = writable({}); // { uniqueName: 'LOADING' | 'ERROR' | 'SUCCESS' }
// Returns a boolean if any model has a status of loading
export const modelsLoading = derived(statusOfModels, statusObj => {
return Object.values(statusObj).includes('LOADING');
})
// Updates a model's status based on its unique name
export const updateModelStatus = (name, status) => {
statusOfModels.update(current => {
return {
...current,
[name]: status,
}
})
}
// List of example model URLs
export const modelURL = {
littleCity: 'https://threejs.org/manual/examples/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf',
mountains: 'https://threejs.org/manual/examples/resources/models/mountain_landscape/scene.gltf',
llama: 'https://threejs.org/manual/examples/resources/models/animals/Llama.gltf',
pug: 'https://threejs.org/manual/examples/resources/models/animals/Pug.gltf',
sheep: 'https://threejs.org/manual/examples/resources/models/animals/Sheep.gltf',
}
/*
Models taken from the threejs examples.
Little City model created by antonmoek:
https://sketchfab.com/antonmoek
*/
Далее мы создадим новый компонент svelte под названием ReusableGLTF.svelte
, который будет принимать некоторые реквизиты и испускать пользовательское событие для обмена статусом загрузки со своим родительским компонентом.
<script>
import * as SC from 'svelte-cubed';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { createEventDispatcher } from 'svelte';
import { onMount } from 'svelte';
// Component Props
export let modelURL;
export let scale = [1, 1, 1];
export let position = [0, 0, 0];
export let rotation = [0, 0, 0];
export let name = 'UniqueName_' + Math.random() + Date.now();
let model = null;
// Custom Event to track loading status from parent
const dispatch = createEventDispatcher();
function emitStatus(status){
dispatch('statusChange', {name, status});
}
function loadGLTF() {
const loader = new GLTFLoader();
return loader.loadAsync(modelURL)
}
onMount(() => {
if (modelURL) {
emitStatus('LOADING');
loadGLTF()
.then(_model => {
model = _model;
emitStatus('SUCCESS');
})
.catch(err => {
console.error('Error loading model:', name, err)
emitStatus('ERROR');
})
}
})
</script>
{#if model}
<SC.Primitive
object={model.scene}
{scale}
{position}
{rotation}
/>
{/if}
Должен выглядеть довольно похоже на наш предыдущий компонент LittleCity.svelte
! Теперь я знаю, что вы думаете: «Math.random() — это красный флаг!». И вы правы, эта реализация была бы намного сильнее с uuid вместо имени для отслеживания состояния загрузки. Сделайте это! Вы можете использовать для этого пакет uuid и обновить шаблоны загрузки, чтобы использовать id вместо имени. Давайте жить опасно и продолжим.
В нашем компоненте App.svelte
импортируем ReusableGLTF.svelte
, импортируем modelURL
, а затем удалим наш компонент LittleCity.svelte
и вместо него используем наш новый компонент!
<script>
// … other imports
import ReusableGLTF from './ReusableGLTF.svelte';
import { modelURL } from './stores';
// …
</script>
<SC.Canvas>
<!-- … all the scene stuff is here -->
<!-- Don't need this once we have a reusable component! -->
<!-- <LittleCity /> -->
<ReusableGLTF
modelURL={modelURL['littleCity']}
name="littleCity"
scale={[.05,.05,.05]}
/>
</SC.Canvas>
Вот оно! Давайте добавим еще немного…
<ReusableGLTF
modelURL={modelURL['llama']}
name="llama"
position={[-6, 17, 0]}
rotation={[0, Math.PI * 1.25, 0]}
/>
<ReusableGLTF
modelURL={modelURL['pug']}
name="pug"
position={[0, 17, 0]}
/>
<ReusableGLTF
modelURL={modelURL['sheep']}
name="sheep"
position={[-6, 17, 4]}
rotation={[0, Math.PI * 1.25, 0]}
/>
Герои, которые нужны нашему городу!
Наконец, давайте обработаем загрузку, и на этом все (пока). Помните, что каждый компонент ReusableGLTF
испускает пользовательское событие statusChange
. Подробнее о пользовательских событиях здесь. Давайте обновим наш App.js
для обработки этого события путем импорта из store.js
и создания новой функции handle:
import { statusOfModels, modelURL, modelsLoading, updateModelStatus } from './stores';
function handleStatusChange(evt) {
updateModelStatus(evt.detail.name, evt.detail.status);
}
Дополнительно: Обработка состояния загрузки
Теперь добавьте слушателя и обработчик события к каждому компоненту ReusableGLTF
(или хотя бы к любому компоненту, для которого вы хотите отслеживать загрузку) следующим образом:
<ReusableGLTF
modelURL={modelURL['littleCity']}
name="littleCity"
scale={[.05,.05,.05]}
on:statusChange={handleStatusChange}
/>
Вот это хорошие данные! Не стесняйтесь вставлять консольные журналы, чтобы посмотреть, что происходит, или просто используйте $: console.log("statuses:", $statusOfModels)
, чтобы получить журнал при любом обновлении.
Вооружившись этими данными, мы можем создать новый компонент Loading.svelte
, который принимает один реквизит:
<script>
export let showLoading = false;
</script>
{#if showLoading}
<div class="loading-container">
<p>
Loading...
</p>
</div>
{/if}
<style>
.loading-container {
position: fixed;
left: 1rem;
bottom: 1rem;
background: #00000088;
color: #fafbfc;
padding: .5rem .875rem;
}
p {
margin: 0;
}
</style>
Давайте импортируем его и бросим в наш компонент App.svelte
. Мы уже настроили магазин, который возвращает булево значение, если статус любой модели === loading, поэтому мы передадим его в качестве реквизита:
<script>
// … other imports
import Loading from './Loading.svelte';
// …
</script>
<Loading showLoading={$modelsLoading} />
Аааа, и мы закончили! Многое из того, что здесь происходит, может быть улучшено и приспособлено для ваших нужд, но теперь у нас есть строительные блоки для добавления и комбинирования моделей glTF/glb. Вы можете построить целый музей или продемонстрировать свои собственные модели. Мы еще даже не говорили об анимации… Может быть, в следующий раз.
Делитесь своими улучшениями и реализациями в комментариях! Увидеть, что строят другие люди, — это огромный источник вдохновения.
Ресурсы
REPL: https://svelte.dev/repl/8ea0488302bb434991cc5b82f653cdb5?version=3.48.0
App.svelte
<script>
import * as THREE from 'three';
import * as SC from 'svelte-cubed';
import LittleCity from './LittleCity.svelte';
import ReusableGLTF from './ReusableGLTF.svelte';
import Loading from './Loading.svelte';
import { statusOfModels, modelURL, modelsLoading, updateModelStatus } from './stores';
function handleStatusChange(evt) {
updateModelStatus(evt.detail.name, evt.detail.status);
}
</script>
<SC.Canvas
background={new THREE.Color("skyblue")}
antialias
>
<SC.PerspectiveCamera
position={[-10, 36, 20]}
near={0.1}
far={500}
fov={40}
/>
<SC.OrbitControls
enabled={true}
enableZoom={true}
autoRotate={false}
autoRotateSpeed={2}
enableDamping={true}
dampingFactor={0.1}
target={[-6, 17, 0]}
/>
<SC.DirectionalLight
color={new THREE.Color(0xffffff)}
position={[0,10,10]}
intensity={0.75}
shadow={false}
/>
<SC.AmbientLight
color={new THREE.Color(0xffffff)}
intensity={0.75}
/>
<!-- Don't need this once we have a reusable component! -->
<!-- <LittleCity /> -->
<ReusableGLTF
modelURL={modelURL['littleCity']}
name="littleCity"
scale={[.05,.05,.05]}
on:statusChange={handleStatusChange}
/>
<ReusableGLTF
modelURL={modelURL['llama']}
name="llama"
position={[-6, 17, 0]}
rotation={[0, Math.PI * 1.25, 0]}
on:statusChange={handleStatusChange}
/>
<ReusableGLTF
modelURL={modelURL['pug']}
name="pug"
position={[0, 17, 0]}
on:statusChange={handleStatusChange}
/>
<ReusableGLTF
modelURL={modelURL['sheep']}
name="sheep"
position={[-6, 17, 4]}
rotation={[0, Math.PI * 1.25, 0]}
on:statusChange={handleStatusChange}
/>
</SC.Canvas>
<Loading showLoading={$modelsLoading} />
ReusableGLTF.svelte
<script>
import * as SC from 'svelte-cubed';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { createEventDispatcher } from 'svelte';
import { onMount } from 'svelte';
// Component Props
export let modelURL;
export let scale = [1, 1, 1];
export let position = [0, 0, 0];
export let rotation = [0, 0, 0];
export let name = 'UniqueName_' + Math.random() + Date.now();
let model = null;
// Custom Event to track loading status from parent
const dispatch = createEventDispatcher();
function emitStatus(status){
dispatch('statusChange', {name, status});
}
function loadGLTF() {
const loader = new GLTFLoader();
return loader.loadAsync(modelURL)
}
onMount(() => {
if (modelURL) {
emitStatus('LOADING');
loadGLTF()
.then(_model => {
model = _model;
emitStatus('SUCCESS');
})
.catch(err => {
console.error('Error loading model:', name, err)
emitStatus('ERROR');
})
}
})
</script>
{#if model}
<SC.Primitive
object={model.scene}
{scale}
{position}
{rotation}
/>
{/if}
Loading.svelte
<script>
export let showLoading = false;
</script>
{#if showLoading}
<div class="loading-container">
<p>
Loading...
</p>
</div>
{/if}
<style>
.loading-container {
position: fixed;
left: 1rem;
bottom: 1rem;
background: #00000088;
color: #fafbfc;
padding: .5rem .875rem;
}
p {
margin: 0;
}
</style>
stores.js
import { writable, readable, derived } from 'svelte/store';
// Contains the status of all models
export const statusOfModels = writable({}); // { uniqueName: 'LOADING' | 'ERROR' | 'SUCCESS' }
// Returns a boolean if any model has a status of loading
export const modelsLoading = derived(statusOfModels, statusObj => {
return Object.values(statusObj).includes('LOADING');
})
// Updates a model's status based on its unique name
export const updateModelStatus = (name, status) => {
statusOfModels.update(current => {
return {
...current,
[name]: status,
}
})
}
// List of example model URLs
export const modelURL = {
littleCity: 'https://threejs.org/manual/examples/resources/models/cartoon_lowpoly_small_city_free_pack/scene.gltf',
mountains: 'https://threejs.org/manual/examples/resources/models/mountain_landscape/scene.gltf',
llama: 'https://threejs.org/manual/examples/resources/models/animals/Llama.gltf',
pug: 'https://threejs.org/manual/examples/resources/models/animals/Pug.gltf',
sheep: 'https://threejs.org/manual/examples/resources/models/animals/Sheep.gltf',
}