Svelte-Cubed: Загрузка ваших моделей glTF

Эта статья является четвертой в серии статей для начинающих о создании 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. Поиск крутых моделей
  2. Создание компонента Svelte для модели
  3. Импортируйте GLTFLoader из threejs
  4. Загрузите модель onMount.
  5. Условно передайте ее в компонент 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 добавьте две переменные:

  1. URL к файлу glTF
  2. Пустая переменная, которая будет содержать модель, когда мы ее получим.
<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',
}
Войти в полноэкранный режим Выход из полноэкранного режима

Оцените статью
Procodings.ru
Добавить комментарий