3D-аудио — источники, зоны, слушатель
Звук, размещённый в 3D-мире. Аспект AudioSource делает узел излучателем, за которым движок следует
каждый кадр; audio.playAt запускает одиночный звук в точке; аспект AudioZone — объём
реверберации; а scene.audio (SceneAudio, который несёт каждая Scene) выбирает слушателя —
активную камеру, если ты не выберешь узел. Клипы, голоса и шины — из
игрового аудио, как и гейт хоста: без audio.supported аспекты
прикрепляются и ничего не делают (play возвращает голос, который уже закончился).
Обзор
const main = async () => {
const hum = await Sound.load(asset('./sfx/hum.wav')) // моно: то, что источник размещает в пространстве
const knock = await Sound.load(asset('./sfx/click.wav'))
const scene = new Scene()
scene.camera.position = [0, 1.6, 4]
const generator = new Node().aspect(AudioSource, { minDistance: 2, maxDistance: 40 })
generator.position = [0, 1, -6]
const hall = new Node().aspect(AudioZone, { box: [8, 4, 8], reverb: 'hall', blend: 2 })
scene.add(generator, hall)
scene.open()
const loop = generator.audio.play(hum, { loop: true }) // следует за узлом, слышен от камеры
loop.volume = 0.8
audio.playAt(knock, [3, 0, -2], { maxDistance: 25 }) // одиночный звук в мировой точке
}
main()AudioSource — излучатель на узле
node.aspect(AudioSource, {
minDistance?: number // метры полной громкости вокруг узла; по умолчанию 1
maxDistance?: number // метры, за которыми он не становится тише; по умолчанию 50
rolloff?: Rolloff // 'inverse' (по умолчанию) | 'linear' | 'exponential' | 'none'
cone?: { inner: number, outer: number, outerGain?: number } | null // градусы; по умолчанию null (во все стороны)
doppler?: number // 0 (выкл, по умолчанию) … 1
spread?: number // 0 точечное панорамирование (по умолчанию) … 1 одинаково в обоих ушах
occlusion?: boolean // глушить, когда на пути физическое тело; по умолчанию false
bus?: string // шина его голосов по умолчанию; по умолчанию 'sfx'
})
node.audio.play(sound: Sound, options?: PlaySoundOptions): Voice // опции audio.play
node.audio.stopAll(fade?: number) // его голоса; fade в секундах
node.audio.voices // сколько играетКаждая опция — ещё и живое свойство (node.audio.maxDistance = 80), которое доходит до уже играющих
голосов — кроме bus, который выбирает шину для последующих запусков. Движок читает мировую позу
узла каждый кадр, после поздней фазы — подвешенные, анимированные и физически управляемые узлы просто
работают; вперёд — −Z.
аксессор — node.audio — не то же самое, что глобальный audio и scene.audio.
Расстояние спадает между minDistance и maxDistance (полная громкость внутри minDistance, никаких
изменений за maxDistance):
inverse— усиление =minDistance / distance; при умолчаниях 0.02 на 50 м.linear— от 1 наminDistanceпрямо вниз до тишины наmaxDistance.exponential—(distance / minDistance)^-1: с фиксированным фактором спада движка, равным 1, это та же кривая, чтоinverse.none— без затухания по расстоянию.
cone делает динамик, сирену, голос, отвёрнутый в сторону: inner и outer — полные углы в градусах
вокруг −Z узла; внутри inner громкость 1, за outer — outerGain (по умолчанию 0.25), между ними —
смешивание.
doppler требует, чтобы узел действительно двигался: его скорость измеряется по его движению каждый
кадр (с потолком 60 м/с, так что телепорт не свистит); scene.audio.dopplerFactor масштабирует
doppler каждого источника.
occlusion бросает три луча каждый кадр от источника к слушателю (в его центр и на 0.5 м по обе
стороны) — до 8 помеченных источников за кадр, по очереди — и глушит голоса по тому, сколько лучей
перекрыто, вплоть до low-pass на 800 Гц и −12 дБ, когда перекрыты все три, со сглаживанием около
0.1 с. Перекрывает физическое тело, статическое или движущееся (Shape с Physics) — не Trigger, не
Shape сам по себе и никогда не собственное тело узла-источника или его потомков. Нужна 3D-физика
хоста (Physics.supported).
Источник на стороне движка существует, пока аспект прикреплён: его отсоединение или уничтожение узла останавливает его голоса (fade 50 мс).
Одиночные звуки в точке — audio.playAt
audio.playAt(sound: Sound, position: Vec3Like, options?: PlaySoundOptions & {
minDistance?: number // по умолчанию 1
maxDistance?: number // по умолчанию 50
rolloff?: Rolloff // по умолчанию 'inverse'
}): VoiceУдары, рикошеты, обломки: собственный источник в фиксированной мировой позиции (метры), освобождаемый,
когда голос заканчивается. bus по умолчанию sfx; конуса, доплера и окклюзии нет, а pan ничего не
делает.
Зоны реверберации — AudioZone
node.aspect(AudioZone, {
box?: Vec3Like // полуразмеры, в локальных единицах узла
sphere?: number // радиус (установка одного сбрасывает другое)
reverb?: ReverbPreset | ReverbParams // по умолчанию 'room'
blend?: number // метры кроссфейда внутри границы; по умолчанию 1, 0 = жёсткий край
bus?: string // шина, на которую идёт её реверберация; по умолчанию 'sfx'
})
node.audioZone.id // то, что сообщает audio.stats.listenerZone, пока слушатель внутриПока слушатель внутри, реверберация зоны заменяет собственный reverb шины (см.
Шины), нарастая на первых blend метрах за границей; вне всех зон
снова действует собственный шины. Зоны вкладываются: побеждает самая маленькая, содержащая слушателя.
Объём следует за узлом — его позицией, вращением и масштабом.
Без box и sphere зона берёт сферу или бокс Shape узла (читается при прикреплении зоны —
прикрепи Shape первым), иначе полуразмеры 1 (куб 2 м).
const corridor = new Node().aspect(Shape, { box: [1, 1.5, 8] }).aspect(AudioZone, { reverb: 'bathroom' })audio.stats.listenerZone / zoneBlend показывают, в какой зоне слушатель и насколько глубоко (0 … 1).
Слушатель — scene.audio
scene.audio.listener: Node | null // по умолчанию null: камера, которой рендерится сцена
scene.audio.dopplerFactor: number // умножает doppler каждого источника; по умолчанию 1, 0 = выкл вездеСлушатель слышит из своей мировой позиции, лицом к −Z, вверх +Y. По умолчанию это камера — follow-риг или переключение камеры ничего не меняют. Задай узел, чтобы слушать из другого места: голова персонажа в игре от третьего лица, чтобы звуки панорамировались вокруг персонажа, а не камеры.
у движка один слушатель. Узел, заданный на одной сцене, остаётся слушателем после открытия другой
сцены — чей scene.audio.listener по-прежнему читается как null — пока ты не задашь
listener = null или узел не будет уничтожен; тогда снова слушает рендерящая камера.
Наушники — audio.hrtf
audio.hrtf = true // бинауральный рендеринг; по умолчанию false
audio.hrtfVoices = 16 // сколько ближайших 3D-голосов его получают; по умолчанию 16
audio.stats.hrtfVoices // сколько сейчас рендерится бинауральноС включённым hrtf ближайшие 3D-голоса фильтруются на каждое ухо (набор MIT KEMAR), так что у звука
появляются настоящие верх, низ и «сзади» — в наушниках; на колонках это только размазывает картину,
потому по умолчанию выключено. Каждый такой голос стоит больше CPU; остальные остаются на простом
панорамировании, а 2D-голоса никогда не бинауральны. Хост, собранный без фильтров (iOS App Clip),
держит каждый голос панорамированным: audio.stats.hrtfVoices там остаётся 0, что бы ни читал
audio.hrtf.
Смотрите также
- Игровое аудио —
Sound,Voice, шины, пул голосов,audio.supported - Аспекты —
node.aspect(Class, opts)и отсоединение - Physics — тела, которые перекрывают звук
- Scene — камера, которая слушает по умолчанию