Skip to content

Sound Blocks - EP01:前言

sound-blocks

平常上班時耳機都常都是放白噪音、雨聲等等自然聲音。

同一個聲音聽太多次,連下一個雷聲甚麼時候出現我都快背起來了。(›´ω`‹ )

雖然有很多不錯的線上混音器網站,但是總感覺差了一點甚麼。

這次想嘗試用 Babylon.js 做一個 3D 白噪音混音器,聲音使用隨機播放的形式呈現。

說明

做一個有趣的白噪音網站,同時練習更複雜的 3D 應用

概念

每個聲音都是一個積木,積木可以任意組合,積木種類與規模會產生不同自然音效。

例如:

  • 草:無
  • 樹木:風吹過樹葉聲音
  • 雲:下雨聲音
  • 房子:咖啡廳聲音
  • 河:流水聲

不同規模會產生生態系,例如樹木夠多會有蟲鳴、鳥叫等等。

地圖資訊會儲存在 URL 中,分享連結即可讓其他人聽到你的設計的自然音效。

素材

kaykit-medieval-hexagon-sample1

3D 積木則基於 Kay Lousberg 大大設計的「KayKit - Medieval Hexagon Pack

感謝 Kay Lousberg 大大提供如此優秀的素材!(*´∀`)~♥

開發

現在讓我們開始吧。੭ ˙ᗜ˙ )੭

第一步我打算先在此部落格進行原型測試。

歸功於 VitePress 強大的客製化能力,現在讓我們來建立一個完全客製化的頁面。

建立頁面

新增檔案。

content\aquarium\sound-blocks\index.md

markdown
---
title: 聲音積木
image: https://codlin.me/cod-toys.webp
layout: false
---

layout 設為 false 代表不使用預設的佈局,這樣就可以自由設計頁面了!♪( ◜ω◝و(و

TIP

詳細說明可見文件:VitePress:Layout

現在導航到 http://localhost:3000/aquarium/sound-blocks/ 應該會是一片空白。

讓我們新增一個 Vue 元件作為首頁。

content\aquarium\sound-blocks\sound-blocks.vue

vue
<template>
  <div class=" overflow-hidden">
    <canvas
      class="canvas w-full h-full"
    />
  </div>
</template>

<script setup lang="ts">
onMounted(() => {
  document.body.classList.add('overflow-hidden')
})
onUnmounted(() => {
  document.body.classList.remove('overflow-hidden')
})
</script>

<style lang="sass">
body.overflow-hidden
  overflow: hidden
</style>

<style lang="sass" scoped>
.canvas
  outline: none
</style>

預留一個 canvas 之後用於 Babylon.js 渲染。

body 上加上 overflow-hidden 是為了避免頁面出現滾動條。

接著在頁面中引入元件。

content\aquarium\sound-blocks\index.md

markdown
---
title: 聲音積木
image: https://codlin.me/cod-toys.webp
layout: false
---

<script setup> 
import SoundBlocks from './sound-blocks.vue'
</script>

<sound-blocks />

現在讓我們來產生 3D 場景吧。

由於 babylon 場景最基本構成是 enginescenecameralight

所以我將相關邏輯直接獨立為 use-babylon-scene,簡化元件內容。

程式碼內容就不贅述了,有興趣的可以參考這裡

現在來引用 use-babylon-scene

content\aquarium\sound-blocks\sound-blocks.vue

vue
// ...

<script setup lang="ts">
import { useBabylonScene } from './use-babylon-scene'

const { 
  canvasRef, 
} = useBabylonScene() 

// ...
</script>

// ...

接著產生地板與霧氣效果。

content\aquarium\sound-blocks\sound-blocks.vue

vue
// ...

<script setup lang="ts">
import type { Scene } from '@babylonjs/core'
import {
  Color3,
  MeshBuilder,
  StandardMaterial,
} from '@babylonjs/core'
import { useBabylonScene } from './use-babylon-scene'

function createGround({ scene }: {
  scene: Scene;
}) {
  const ground = MeshBuilder.CreateGround('ground', {
    width: 1000,
    height: 1000,
  }, scene)

  const groundMaterial = new StandardMaterial('groundMaterial', scene)
  groundMaterial.diffuseColor = new Color3(0.98, 0.98, 0.98)
  ground.material = groundMaterial

  ground.receiveShadows = true
}

const {
  canvasRef,
} = useBabylonScene({
  async init(params) {
    const { scene } = params
    createGround({ scene })
  },
})

// ...
</script>

// ...

現在場景中應該會有一個白色地板與灰背景效果了。

接著把模型放到靜態目錄中,下載模型並解壓縮。

content\public\sound-blocks\hexagon-pack

來建立第一個積木吧!

首先建立一個統一匯出產生積木 function 的檔案,並定義產生積木的 params 型別。

content\aquarium\sound-blocks\blocks\index.ts

typescript
import type { Scene, ShadowGenerator } from '@babylonjs/core'

export interface CreateBlockParams {
  scene: Scene;
  shadowGenerator?: ShadowGenerator;
}

產生第一個樹木積木。

content\aquarium\sound-blocks\blocks\tree.ts

typescript
import type { CreateBlockParams } from '.'
import {
  SceneLoader,
  TransformNode,
  Vector3,
} from '@babylonjs/core'
import { forEach, pipe } from 'remeda'

export async function createTreeBlock(
  {
    scene,
    shadowGenerator,
  }: CreateBlockParams,
) {
  const [
    treeResult,
    hexResult,
  ] = await Promise.all([
    SceneLoader.ImportMeshAsync(
      '',
      '/sound-blocks/hexagon-pack/decoration/nature/',
      'trees_B_medium.gltf',
      scene,
    ),
    SceneLoader.ImportMeshAsync(
      '',
      '/sound-blocks/hexagon-pack/tiles/base/',
      'hex_grass.gltf',
      scene,
    ),
  ])

  const rootNode = new TransformNode('block-root', scene)

  pipe(
    treeResult.meshes,
    forEach((mesh) => {
      mesh.receiveShadows = true
    }),
    ([rootMesh]) => {
      if (!rootMesh)
        return

      rootMesh.position = new Vector3(0, 0.5, 0)
      shadowGenerator?.addShadowCaster(rootMesh)

      rootMesh.parent = rootNode
    },
  )

  pipe(
    hexResult.meshes,
    forEach((mesh) => {
      mesh.receiveShadows = true
    }),
    ([rootMesh]) => {
      if (!rootMesh)
        return

      rootMesh.position = new Vector3(0, 0.5, 0)
      shadowGenerator?.addShadowCaster(rootMesh)

      rootMesh.parent = rootNode
    },
  )

  return {
    rootNode,
  }
}

並在 index.ts 統一匯出。

content\aquarium\sound-blocks\blocks\index.ts

typescript
import type { Scene, ShadowGenerator } from '@babylonjs/core'

export interface CreateBlockParams {
  scene: Scene;
  shadowGenerator?: ShadowGenerator;
}

export * from './tree'

最後在元件引用看看。

content\aquarium\sound-blocks\sound-blocks.vue

vue
// ...

<script setup lang="ts">
// ...

const {
  canvasRef,
} = useBabylonScene({
  async init(params) {
    const { scene } = params
    const shadowGenerator = createShadowGenerator(scene)

    createGround({ scene })

    await createTreeBlock({ scene, shadowGenerator }) 
  },
})

// ...
</script>

// ...

沒意外的話,畫面裡會跑出一個精緻的樹木積木。

first-tree-block

成功踏出第一步!✧⁑。٩(ˊᗜˋ*)و✧⁕。

總結 🐟

以上程式碼可以在此取得

  • 建立 3D 場景
  • 建立第一個積木
如果我的文章對您有所啟發或幫助,歡迎「 請我喝一杯咖啡 」或花 10 秒登入 LikeCoin 按下方按鈕拍手鼓勵我喔!
鱈魚感謝您! (´▽`ʃ💖ƪ)

更新於: