audio clips reworked
This commit is contained in:
parent
882ca23d45
commit
b5b8657792
7 changed files with 140 additions and 352 deletions
|
@ -22,13 +22,6 @@ static const ComponentType AMBIENT_SOUND_TYPE = Reflection::getComponentType("am
|
||||||
static const ComponentType ECHO_ZONE_TYPE = Reflection::getComponentType("echo_zone");
|
static const ComponentType ECHO_ZONE_TYPE = Reflection::getComponentType("echo_zone");
|
||||||
static const ComponentType CHORUS_ZONE_TYPE = Reflection::getComponentType("chorus_zone");
|
static const ComponentType CHORUS_ZONE_TYPE = Reflection::getComponentType("chorus_zone");
|
||||||
|
|
||||||
|
|
||||||
enum class AudioSceneVersion : int
|
|
||||||
{
|
|
||||||
LAST
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
struct Listener
|
struct Listener
|
||||||
{
|
{
|
||||||
EntityPtr entity;
|
EntityPtr entity;
|
||||||
|
@ -38,7 +31,7 @@ struct Listener
|
||||||
struct AmbientSound
|
struct AmbientSound
|
||||||
{
|
{
|
||||||
EntityRef entity;
|
EntityRef entity;
|
||||||
AudioScene::ClipInfo* clip;
|
Clip* clip = nullptr;
|
||||||
bool is_3d;
|
bool is_3d;
|
||||||
int playing_sound;
|
int playing_sound;
|
||||||
};
|
};
|
||||||
|
@ -48,17 +41,22 @@ struct PlayingSound
|
||||||
{
|
{
|
||||||
AudioDevice::BufferHandle buffer_id;
|
AudioDevice::BufferHandle buffer_id;
|
||||||
EntityPtr entity;
|
EntityPtr entity;
|
||||||
AudioScene::ClipInfo* clip;
|
Clip* clip = nullptr;
|
||||||
bool is_3d;
|
bool is_3d;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
struct AudioSceneImpl final : AudioScene
|
struct AudioSceneImpl final : AudioScene
|
||||||
{
|
{
|
||||||
|
enum class Version : i32 {
|
||||||
|
INIT,
|
||||||
|
CLIPS_REWORKED,
|
||||||
|
LATEST
|
||||||
|
};
|
||||||
|
|
||||||
AudioSceneImpl(AudioSystem& system, Universe& context, IAllocator& allocator)
|
AudioSceneImpl(AudioSystem& system, Universe& context, IAllocator& allocator)
|
||||||
: m_allocator(allocator)
|
: m_allocator(allocator)
|
||||||
, m_universe(context)
|
, m_universe(context)
|
||||||
, m_clips(allocator)
|
|
||||||
, m_system(system)
|
, m_system(system)
|
||||||
, m_device(system.getDevice())
|
, m_device(system.getDevice())
|
||||||
, m_ambient_sounds(allocator)
|
, m_ambient_sounds(allocator)
|
||||||
|
@ -90,34 +88,20 @@ struct AudioSceneImpl final : AudioScene
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int getVersion() const override { return (int)AudioSceneVersion::LAST; }
|
i32 getVersion() const override { return (i32)Version::LATEST; }
|
||||||
|
|
||||||
|
|
||||||
void clear() override
|
void clear() override
|
||||||
{
|
{
|
||||||
for (auto* clip : m_clips)
|
|
||||||
{
|
|
||||||
clip->clip->decRefCount();
|
|
||||||
LUMIX_DELETE(m_allocator, clip);
|
|
||||||
}
|
|
||||||
m_clips.clear();
|
|
||||||
m_ambient_sounds.clear();
|
m_ambient_sounds.clear();
|
||||||
m_echo_zones.clear();
|
m_echo_zones.clear();
|
||||||
m_chorus_zones.clear();
|
m_chorus_zones.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int playSound(EntityRef entity, const char* clip_name, bool is_3d) override
|
|
||||||
{
|
|
||||||
auto* clip = getClipInfo(clip_name);
|
|
||||||
if (clip) return play(entity, clip, is_3d);
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateAnimationEvents()
|
void updateAnimationEvents()
|
||||||
{
|
{
|
||||||
if (!m_animation_scene) return;
|
/*if (!m_animation_scene) return;
|
||||||
|
|
||||||
InputMemoryStream blob(m_animation_scene->getEventStream());
|
InputMemoryStream blob(m_animation_scene->getEventStream());
|
||||||
u32 sound_type = crc32("sound");
|
u32 sound_type = crc32("sound");
|
||||||
|
@ -143,7 +127,7 @@ struct AudioSceneImpl final : AudioScene
|
||||||
{
|
{
|
||||||
blob.skip(size);
|
blob.skip(size);
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
void update(float time_delta, bool paused) override
|
void update(float time_delta, bool paused) override
|
||||||
|
@ -170,8 +154,8 @@ struct AudioSceneImpl final : AudioScene
|
||||||
m_device.setSourcePosition(sound.buffer_id, pos);
|
m_device.setSourcePosition(sound.buffer_id, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto* clip_info = sound.clip;
|
Clip* clip_info = sound.clip;
|
||||||
if (!clip_info->looped && m_device.isEnd(sound.buffer_id))
|
if (!clip_info->m_looped && m_device.isEnd(sound.buffer_id))
|
||||||
{
|
{
|
||||||
m_device.stop(sound.buffer_id);
|
m_device.stop(sound.buffer_id);
|
||||||
sound.buffer_id = AudioDevice::INVALID_BUFFER_HANDLE;
|
sound.buffer_id = AudioDevice::INVALID_BUFFER_HANDLE;
|
||||||
|
@ -231,37 +215,20 @@ struct AudioSceneImpl final : AudioScene
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ClipInfo* getAmbientSoundClip(EntityRef entity) override
|
Path getAmbientSoundClip(EntityRef entity) override
|
||||||
{
|
{
|
||||||
return m_ambient_sounds[entity].clip;
|
AmbientSound& snd = m_ambient_sounds[entity];
|
||||||
|
return snd.clip ? snd.clip->getPath() : Path();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int getClipInfoIndex(ClipInfo* info) override
|
void setAmbientSoundClip(EntityRef entity, const Path& clip) override
|
||||||
{
|
{
|
||||||
for (int i = 0; i < m_clips.size(); ++i)
|
Clip* res = m_system.getEngine().getResourceManager().load<Clip>(clip);
|
||||||
{
|
if (m_ambient_sounds[entity].clip) {
|
||||||
if (m_clips[i] == info) return i;
|
m_ambient_sounds[entity].clip->decRefCount();
|
||||||
}
|
}
|
||||||
return -1;
|
m_ambient_sounds[entity].clip = res;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int getAmbientSoundClipIndex(EntityRef entity) override
|
|
||||||
{
|
|
||||||
return m_clips.indexOf(m_ambient_sounds[entity].clip);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void setAmbientSoundClipIndex(EntityRef entity, int index) override
|
|
||||||
{
|
|
||||||
m_ambient_sounds[entity].clip = m_clips[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void setAmbientSoundClip(EntityRef entity, ClipInfo* clip) override
|
|
||||||
{
|
|
||||||
m_ambient_sounds[entity].clip = clip;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -345,22 +312,11 @@ struct AudioSceneImpl final : AudioScene
|
||||||
void serialize(OutputMemoryStream& serializer) override
|
void serialize(OutputMemoryStream& serializer) override
|
||||||
{
|
{
|
||||||
serializer.write(m_listener.entity);
|
serializer.write(m_listener.entity);
|
||||||
serializer.write(m_clips.size());
|
|
||||||
for (auto* clip : m_clips)
|
|
||||||
{
|
|
||||||
serializer.write(clip != nullptr);
|
|
||||||
if (!clip) continue;
|
|
||||||
|
|
||||||
serializer.write(clip->volume);
|
|
||||||
serializer.write(clip->looped);
|
|
||||||
serializer.writeString(clip->name);
|
|
||||||
serializer.writeString(clip->clip->getPath().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
serializer.write(m_ambient_sounds.size());
|
serializer.write(m_ambient_sounds.size());
|
||||||
for (const AmbientSound& sound : m_ambient_sounds)
|
for (const AmbientSound& sound : m_ambient_sounds)
|
||||||
{
|
{
|
||||||
serializer.write(m_clips.indexOf(sound.clip));
|
serializer.writeString(sound.clip ? sound.clip->getPath().c_str() : "");
|
||||||
serializer.write(sound.entity);
|
serializer.write(sound.entity);
|
||||||
serializer.write(sound.is_3d);
|
serializer.write(sound.is_3d);
|
||||||
}
|
}
|
||||||
|
@ -387,36 +343,20 @@ struct AudioSceneImpl final : AudioScene
|
||||||
m_universe.onComponentCreated((EntityRef)m_listener.entity, LISTENER_TYPE, this);
|
m_universe.onComponentCreated((EntityRef)m_listener.entity, LISTENER_TYPE, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
int count = 0;
|
if (version < (i32)Version::CLIPS_REWORKED) {
|
||||||
serializer.read(count);
|
int dummy;
|
||||||
const u32 clip_offset = m_clips.size();
|
serializer.read(dummy);
|
||||||
m_clips.reserve(m_clips.size() + count);
|
ASSERT(dummy == 0);
|
||||||
for (int i = 0; i < count; ++i) {
|
|
||||||
bool is_valid;
|
|
||||||
serializer.read(is_valid);
|
|
||||||
if (!is_valid) {
|
|
||||||
m_clips.emplace() = nullptr;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* clip = LUMIX_NEW(m_allocator, ClipInfo);
|
|
||||||
m_clips.emplace() = clip;
|
|
||||||
clip->volume = 1;
|
|
||||||
serializer.read(clip->volume);
|
|
||||||
serializer.read(clip->looped);
|
|
||||||
copyString(clip->name, serializer.readString());
|
|
||||||
clip->name_hash = crc32(clip->name);
|
|
||||||
const char* path = serializer.readString();
|
|
||||||
|
|
||||||
clip->clip = m_system.getEngine().getResourceManager().load<Clip>(Path(path));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i32 count;
|
||||||
serializer.read(count);
|
serializer.read(count);
|
||||||
for (int i = 0; i < count; ++i) {
|
for (int i = 0; i < count; ++i) {
|
||||||
AmbientSound sound;
|
AmbientSound sound;
|
||||||
int clip_idx;
|
ASSERT(version >= (i32)Version::CLIPS_REWORKED);
|
||||||
serializer.read(clip_idx);
|
const char* path = serializer.readString();
|
||||||
if (clip_idx >= 0) sound.clip = m_clips[clip_idx + clip_offset];
|
Clip* res = path[0] ? m_system.getEngine().getResourceManager().load<Clip>(Path(path)) : nullptr;
|
||||||
|
sound.clip = res;
|
||||||
serializer.read(sound.entity);
|
serializer.read(sound.entity);
|
||||||
sound.entity = entity_map.get(sound.entity);
|
sound.entity = entity_map.get(sound.entity);
|
||||||
serializer.read(sound.is_3d);
|
serializer.read(sound.is_3d);
|
||||||
|
@ -447,112 +387,22 @@ struct AudioSceneImpl final : AudioScene
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SoundHandle play(EntityRef entity, const Path& clip, bool is_3d) override {
|
||||||
u32 getClipCount() const override { return m_clips.size(); }
|
Clip* res = m_system.getEngine().getResourceManager().load<Clip>(clip);
|
||||||
|
return play(entity, res, is_3d);
|
||||||
|
|
||||||
const char* getClipName(u32 index) override { return m_clips[index]->name; }
|
|
||||||
|
|
||||||
|
|
||||||
void addClip(const char* name, const Path& path) override
|
|
||||||
{
|
|
||||||
auto* clip = LUMIX_NEW(m_allocator, ClipInfo);
|
|
||||||
copyString(clip->name, name);
|
|
||||||
clip->name_hash = crc32(name);
|
|
||||||
clip->clip = m_system.getEngine().getResourceManager().load<Clip>(path);
|
|
||||||
clip->looped = false;
|
|
||||||
clip->volume = 1;
|
|
||||||
m_clips.push(clip);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SoundHandle play(EntityRef entity, Clip* clip, bool is_3d) {
|
||||||
void removeClip(ClipInfo* info) override
|
for (PlayingSound& sound : m_playing_sounds) {
|
||||||
{
|
if (sound.buffer_id == AudioDevice::INVALID_BUFFER_HANDLE) {
|
||||||
for (PlayingSound& i : m_playing_sounds)
|
|
||||||
{
|
|
||||||
if (i.clip == info && i.buffer_id != AudioDevice::INVALID_BUFFER_HANDLE)
|
|
||||||
{
|
|
||||||
m_device.stop(i.buffer_id);
|
|
||||||
i.buffer_id = AudioDevice::INVALID_BUFFER_HANDLE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (AmbientSound& sound : m_ambient_sounds)
|
|
||||||
{
|
|
||||||
if (sound.clip == info)
|
|
||||||
{
|
|
||||||
sound.clip = nullptr;
|
|
||||||
sound.playing_sound = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Clip* clip = info->clip;
|
|
||||||
if (clip)
|
|
||||||
{
|
|
||||||
clip->decRefCount();
|
|
||||||
}
|
|
||||||
LUMIX_DELETE(m_allocator, info);
|
|
||||||
m_clips.eraseItem(info);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ClipInfo* getClipInfo(const char* name) override
|
|
||||||
{
|
|
||||||
auto hash = crc32(name);
|
|
||||||
for (auto* i : m_clips)
|
|
||||||
{
|
|
||||||
if (i->name_hash == hash) return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ClipInfo* getClipInfo(u32 hash) override
|
|
||||||
{
|
|
||||||
for (auto* i : m_clips)
|
|
||||||
{
|
|
||||||
if (i->name_hash == hash) return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ClipInfo* getClipInfoByIndex(u32 index) override
|
|
||||||
{
|
|
||||||
if (index >= (u32)m_clips.size()) return nullptr;
|
|
||||||
return m_clips[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void setClip(u32 clip_id, const Path& path) override
|
|
||||||
{
|
|
||||||
auto* clip = m_clips[clip_id]->clip;
|
|
||||||
if (clip)
|
|
||||||
{
|
|
||||||
clip->decRefCount();
|
|
||||||
}
|
|
||||||
auto* new_res = m_system.getEngine().getResourceManager().load<Clip>(path);
|
|
||||||
m_clips[clip_id]->clip = static_cast<Clip*>(new_res);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
SoundHandle play(EntityRef entity, ClipInfo* clip_info, bool is_3d) override
|
|
||||||
{
|
|
||||||
for (PlayingSound& sound : m_playing_sounds)
|
|
||||||
{
|
|
||||||
if (sound.buffer_id == AudioDevice::INVALID_BUFFER_HANDLE)
|
|
||||||
{
|
|
||||||
auto* clip = clip_info->clip;
|
|
||||||
if (!clip->isReady()) return INVALID_SOUND_HANDLE;
|
if (!clip->isReady()) return INVALID_SOUND_HANDLE;
|
||||||
|
|
||||||
int flags = is_3d ? (int)AudioDevice::BufferFlags::IS3D : 0;
|
int flags = is_3d ? (int)AudioDevice::BufferFlags::IS3D : 0;
|
||||||
auto buffer = m_device.createBuffer(
|
auto buffer = m_device.createBuffer(clip->getData(), clip->getSize(), clip->getChannels(), clip->getSampleRate(), flags);
|
||||||
clip->getData(), clip->getSize(), clip->getChannels(), clip->getSampleRate(), flags);
|
|
||||||
if (buffer == AudioDevice::INVALID_BUFFER_HANDLE) return INVALID_SOUND_HANDLE;
|
if (buffer == AudioDevice::INVALID_BUFFER_HANDLE) return INVALID_SOUND_HANDLE;
|
||||||
m_device.play(buffer, clip_info->looped);
|
|
||||||
m_device.setVolume(buffer, clip_info->volume);
|
m_device.play(buffer, clip->m_looped);
|
||||||
|
m_device.setVolume(buffer, clip->m_volume);
|
||||||
|
|
||||||
const DVec3 pos = m_universe.getPosition(entity);
|
const DVec3 pos = m_universe.getPosition(entity);
|
||||||
m_device.setSourcePosition(buffer, pos);
|
m_device.setSourcePosition(buffer, pos);
|
||||||
|
@ -560,10 +410,10 @@ struct AudioSceneImpl final : AudioScene
|
||||||
sound.is_3d = is_3d;
|
sound.is_3d = is_3d;
|
||||||
sound.buffer_id = buffer;
|
sound.buffer_id = buffer;
|
||||||
sound.entity = entity;
|
sound.entity = entity;
|
||||||
sound.clip = clip_info;
|
clip->incRefCount();
|
||||||
|
sound.clip = clip;
|
||||||
|
|
||||||
for (const EchoZone& zone : m_echo_zones)
|
for (const EchoZone& zone : m_echo_zones) {
|
||||||
{
|
|
||||||
const double dist2 = (pos - m_universe.getPosition(zone.entity)).squaredLength();
|
const double dist2 = (pos - m_universe.getPosition(zone.entity)).squaredLength();
|
||||||
const double r2 = zone.radius * zone.radius;
|
const double r2 = zone.radius * zone.radius;
|
||||||
if (dist2 > r2) continue;
|
if (dist2 > r2) continue;
|
||||||
|
@ -573,8 +423,7 @@ struct AudioSceneImpl final : AudioScene
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ChorusZone& zone : m_chorus_zones)
|
for (const ChorusZone& zone : m_chorus_zones) {
|
||||||
{
|
|
||||||
const double dist2 = (pos - m_universe.getPosition(zone.entity)).squaredLength();
|
const double dist2 = (pos - m_universe.getPosition(zone.entity)).squaredLength();
|
||||||
double r2 = zone.radius * zone.radius;
|
double r2 = zone.radius * zone.radius;
|
||||||
if (dist2 > r2) continue;
|
if (dist2 > r2) continue;
|
||||||
|
@ -596,6 +445,10 @@ struct AudioSceneImpl final : AudioScene
|
||||||
ASSERT(sound_id >= 0 && sound_id < (int)lengthOf(m_playing_sounds));
|
ASSERT(sound_id >= 0 && sound_id < (int)lengthOf(m_playing_sounds));
|
||||||
m_device.stop(m_playing_sounds[sound_id].buffer_id);
|
m_device.stop(m_playing_sounds[sound_id].buffer_id);
|
||||||
m_playing_sounds[sound_id].buffer_id = AudioDevice::INVALID_BUFFER_HANDLE;
|
m_playing_sounds[sound_id].buffer_id = AudioDevice::INVALID_BUFFER_HANDLE;
|
||||||
|
if (m_playing_sounds[sound_id].clip) {
|
||||||
|
m_playing_sounds[sound_id].clip->decRefCount();
|
||||||
|
m_playing_sounds[sound_id].clip = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -626,7 +479,6 @@ struct AudioSceneImpl final : AudioScene
|
||||||
Listener m_listener;
|
Listener m_listener;
|
||||||
IAllocator& m_allocator;
|
IAllocator& m_allocator;
|
||||||
Universe& m_universe;
|
Universe& m_universe;
|
||||||
Array<ClipInfo*> m_clips;
|
|
||||||
AudioSystem& m_system;
|
AudioSystem& m_system;
|
||||||
PlayingSound m_playing_sounds[AudioDevice::MAX_PLAYING_SOUNDS];
|
PlayingSound m_playing_sounds[AudioDevice::MAX_PLAYING_SOUNDS];
|
||||||
AnimationScene* m_animation_scene = nullptr;
|
AnimationScene* m_animation_scene = nullptr;
|
||||||
|
|
|
@ -50,43 +50,21 @@ struct AudioScene : IScene
|
||||||
using SoundHandle = i32;
|
using SoundHandle = i32;
|
||||||
static constexpr SoundHandle INVALID_SOUND_HANDLE = -1;
|
static constexpr SoundHandle INVALID_SOUND_HANDLE = -1;
|
||||||
|
|
||||||
struct ClipInfo
|
|
||||||
{
|
|
||||||
Clip* clip;
|
|
||||||
char name[30];
|
|
||||||
u32 name_hash;
|
|
||||||
bool looped;
|
|
||||||
float volume = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
static UniquePtr<AudioScene> createInstance(AudioSystem& system,
|
static UniquePtr<AudioScene> createInstance(AudioSystem& system,
|
||||||
Universe& universe,
|
Universe& universe,
|
||||||
struct IAllocator& allocator);
|
struct IAllocator& allocator);
|
||||||
|
|
||||||
virtual void setMasterVolume(float volume) = 0;
|
virtual void setMasterVolume(float volume) = 0;
|
||||||
|
|
||||||
virtual u32 getClipCount() const = 0;
|
|
||||||
virtual const char* getClipName(u32 index) = 0;
|
|
||||||
virtual ClipInfo* getClipInfoByIndex(u32 index) = 0;
|
|
||||||
virtual ClipInfo* getClipInfo(u32 hash) = 0;
|
|
||||||
virtual ClipInfo* getClipInfo(const char* name) = 0;
|
|
||||||
virtual int getClipInfoIndex(ClipInfo* info) = 0;
|
|
||||||
virtual void addClip(const char* name, const Path& path) = 0;
|
|
||||||
virtual void removeClip(ClipInfo* clip) = 0;
|
|
||||||
virtual void setClip(u32 clip_id, const Path& path) = 0;
|
|
||||||
|
|
||||||
virtual EchoZone& getEchoZone(EntityRef entity) = 0;
|
virtual EchoZone& getEchoZone(EntityRef entity) = 0;
|
||||||
virtual ChorusZone& getChorusZone(EntityRef entity) = 0;
|
virtual ChorusZone& getChorusZone(EntityRef entity) = 0;
|
||||||
|
|
||||||
virtual ClipInfo* getAmbientSoundClip(EntityRef entity) = 0;
|
virtual Path getAmbientSoundClip(EntityRef entity) = 0;
|
||||||
virtual int getAmbientSoundClipIndex(EntityRef entity) = 0;
|
virtual void setAmbientSoundClip(EntityRef entity, const Path& clip) = 0;
|
||||||
virtual void setAmbientSoundClipIndex(EntityRef entity, int index) = 0;
|
|
||||||
virtual void setAmbientSoundClip(EntityRef entity, ClipInfo* clip) = 0;
|
|
||||||
virtual bool isAmbientSound3D(EntityRef entity) = 0;
|
virtual bool isAmbientSound3D(EntityRef entity) = 0;
|
||||||
virtual void setAmbientSound3D(EntityRef entity, bool is_3d) = 0;
|
virtual void setAmbientSound3D(EntityRef entity, bool is_3d) = 0;
|
||||||
|
|
||||||
virtual SoundHandle playSound(EntityRef entity, const char* clip_name, bool is_3d) = 0;
|
virtual SoundHandle play(EntityRef entity, const Path& clip, bool is_3d) = 0;
|
||||||
virtual SoundHandle play(EntityRef entity, ClipInfo* clip, bool is_3d) = 0;
|
|
||||||
virtual void stop(SoundHandle sound_id) = 0;
|
virtual void stop(SoundHandle sound_id) = 0;
|
||||||
virtual void setVolume(SoundHandle sound_id, float volume) = 0;
|
virtual void setVolume(SoundHandle sound_id, float volume) = 0;
|
||||||
|
|
||||||
|
|
|
@ -12,25 +12,23 @@
|
||||||
namespace Lumix
|
namespace Lumix
|
||||||
{
|
{
|
||||||
|
|
||||||
|
namespace Reflection {
|
||||||
|
//inline AudioScene::SoundHandle fromVariant(int i, Span<Variant> args, VariantTag<AudioScene::SoundHandle>) { return args[i].i; }
|
||||||
|
}
|
||||||
|
|
||||||
static void registerProperties(IAllocator& allocator)
|
static void registerProperties(IAllocator& allocator)
|
||||||
{
|
{
|
||||||
struct ClipIndexEnum : Reflection::EnumAttribute {
|
|
||||||
u32 count(ComponentUID cmp) const override { return ((AudioScene*)cmp.scene)->getClipCount(); }
|
|
||||||
const char* name(ComponentUID cmp, u32 idx) const override { return ((AudioScene*)cmp.scene)->getClipName(idx); }
|
|
||||||
};
|
|
||||||
|
|
||||||
using namespace Reflection;
|
using namespace Reflection;
|
||||||
static auto audio_scene = scene("audio",
|
static auto audio_scene = scene("audio",
|
||||||
functions(
|
functions(
|
||||||
LUMIX_FUNC(AudioScene::setMasterVolume),
|
LUMIX_FUNC(AudioScene::setMasterVolume),
|
||||||
LUMIX_FUNC(AudioScene::playSound),
|
LUMIX_FUNC(AudioScene::play),
|
||||||
LUMIX_FUNC(AudioScene::setVolume),
|
LUMIX_FUNC(AudioScene::setVolume),
|
||||||
LUMIX_FUNC(AudioScene::setEcho)
|
LUMIX_FUNC(AudioScene::setEcho)
|
||||||
),
|
),
|
||||||
component("ambient_sound",
|
component("ambient_sound",
|
||||||
property("3D", &AudioScene::isAmbientSound3D, &AudioScene::setAmbientSound3D),
|
property("3D", &AudioScene::isAmbientSound3D, &AudioScene::setAmbientSound3D),
|
||||||
property("Sound", LUMIX_PROP(AudioScene, AmbientSoundClipIndex), ClipIndexEnum())
|
property("Sound", LUMIX_PROP(AudioScene, AmbientSoundClip), ResourceAttribute("OGG (*.ogg)", Clip::TYPE))
|
||||||
),
|
),
|
||||||
component("audio_listener"),
|
component("audio_listener"),
|
||||||
component("echo_zone",
|
component("echo_zone",
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#include "engine/profiler.h"
|
#include "engine/profiler.h"
|
||||||
#include "engine/resource.h"
|
#include "engine/resource.h"
|
||||||
#include "engine/string.h"
|
#include "engine/string.h"
|
||||||
|
#include "engine/stream.h"
|
||||||
#define STB_VORBIS_HEADER_ONLY
|
#define STB_VORBIS_HEADER_ONLY
|
||||||
#include "stb/stb_vorbis.cpp"
|
#include "stb/stb_vorbis.cpp"
|
||||||
|
|
||||||
|
@ -25,8 +26,18 @@ void Clip::unload()
|
||||||
bool Clip::load(u64 size, const u8* mem)
|
bool Clip::load(u64 size, const u8* mem)
|
||||||
{
|
{
|
||||||
PROFILE_FUNCTION();
|
PROFILE_FUNCTION();
|
||||||
|
InputMemoryStream blob(mem, size);
|
||||||
|
const u32 version = blob.read<u32>();
|
||||||
|
if (version != 0) return false;
|
||||||
|
|
||||||
|
const Format format = blob.read<Format>();
|
||||||
|
if (format != Format::OGG) return false;
|
||||||
|
|
||||||
|
m_looped = blob.read<bool>();
|
||||||
|
m_volume = blob.read<float>();
|
||||||
|
|
||||||
short* output = nullptr;
|
short* output = nullptr;
|
||||||
auto res = stb_vorbis_decode_memory((unsigned char*)mem, (int)size, &m_channels, &m_sample_rate, &output);
|
auto res = stb_vorbis_decode_memory((unsigned char*)blob.skip(0), (int)(size - blob.getPosition()), &m_channels, &m_sample_rate, &output);
|
||||||
if (res <= 0) return false;
|
if (res <= 0) return false;
|
||||||
|
|
||||||
m_data.resize(res * m_channels);
|
m_data.resize(res * m_channels);
|
||||||
|
|
|
@ -13,6 +13,10 @@ namespace Lumix
|
||||||
struct Clip final : Resource
|
struct Clip final : Resource
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
enum class Format : u8 {
|
||||||
|
OGG
|
||||||
|
};
|
||||||
|
|
||||||
Clip(const Path& path, ResourceManager& manager, IAllocator& allocator)
|
Clip(const Path& path, ResourceManager& manager, IAllocator& allocator)
|
||||||
: Resource(path, manager, allocator)
|
: Resource(path, manager, allocator)
|
||||||
, m_data(allocator)
|
, m_data(allocator)
|
||||||
|
@ -30,6 +34,8 @@ public:
|
||||||
float getLengthSeconds() const { return m_data.size() / float(m_channels * m_sample_rate); }
|
float getLengthSeconds() const { return m_data.size() / float(m_channels * m_sample_rate); }
|
||||||
|
|
||||||
static const ResourceType TYPE;
|
static const ResourceType TYPE;
|
||||||
|
bool m_looped = false;
|
||||||
|
float m_volume = 1;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int m_channels;
|
int m_channels;
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include "editor/world_editor.h"
|
#include "editor/world_editor.h"
|
||||||
#include "engine/crc32.h"
|
#include "engine/crc32.h"
|
||||||
#include "engine/engine.h"
|
#include "engine/engine.h"
|
||||||
|
#include "engine/lua_wrapper.h"
|
||||||
#include "engine/reflection.h"
|
#include "engine/reflection.h"
|
||||||
#include "engine/universe.h"
|
#include "engine/universe.h"
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
struct AssetBrowserPlugin final : AssetBrowser::IPlugin
|
struct AssetBrowserPlugin final : AssetBrowser::IPlugin, AssetCompiler::IPlugin
|
||||||
{
|
{
|
||||||
explicit AssetBrowserPlugin(StudioApp& app)
|
explicit AssetBrowserPlugin(StudioApp& app)
|
||||||
: m_app(app)
|
: m_app(app)
|
||||||
|
@ -32,6 +33,36 @@ struct AssetBrowserPlugin final : AssetBrowser::IPlugin
|
||||||
app.getAssetCompiler().registerExtension("ogg", Clip::TYPE);
|
app.getAssetCompiler().registerExtension("ogg", Clip::TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Meta {
|
||||||
|
bool looped = true;
|
||||||
|
float volume = 1.f;
|
||||||
|
};
|
||||||
|
|
||||||
|
Meta getMeta(const Path& path) const {
|
||||||
|
Meta meta;
|
||||||
|
m_app.getAssetCompiler().getMeta(path, [&path, &meta](lua_State* L){
|
||||||
|
LuaWrapper::getOptionalField(L, LUA_GLOBALSINDEX, "looped", &meta.looped);
|
||||||
|
LuaWrapper::getOptionalField(L, LUA_GLOBALSINDEX, "volume", &meta.volume);
|
||||||
|
});
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool compile(const Path& src) override {
|
||||||
|
FileSystem& fs = m_app.getEngine().getFileSystem();
|
||||||
|
OutputMemoryStream src_data(m_app.getAllocator());
|
||||||
|
if (!fs.getContentSync(src, Ref(src_data))) return false;
|
||||||
|
|
||||||
|
Meta meta = getMeta(src);
|
||||||
|
|
||||||
|
OutputMemoryStream compiled(m_app.getAllocator());
|
||||||
|
compiled.reserve(64 + src_data.size());
|
||||||
|
compiled.write((u32)0);
|
||||||
|
compiled.write(Clip::Format::OGG);
|
||||||
|
compiled.write(meta.looped);
|
||||||
|
compiled.write(meta.volume);
|
||||||
|
compiled.write(src_data.data(), src_data.size());
|
||||||
|
return m_app.getAssetCompiler().writeCompiledResource(src.c_str(), Span(compiled.data(), (i32)compiled.size()));
|
||||||
|
}
|
||||||
|
|
||||||
static AudioDevice& getAudioDevice(Engine& engine)
|
static AudioDevice& getAudioDevice(Engine& engine)
|
||||||
{
|
{
|
||||||
|
@ -56,6 +87,16 @@ struct AssetBrowserPlugin final : AssetBrowser::IPlugin
|
||||||
{
|
{
|
||||||
if(resources.length() > 1) return;
|
if(resources.length() > 1) return;
|
||||||
|
|
||||||
|
if(resources[0]->getPath().getHash() != m_meta_res) {
|
||||||
|
m_meta = getMeta(resources[0]->getPath());
|
||||||
|
m_meta_res = resources[0]->getPath().getHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiEx::Label("Looped");
|
||||||
|
ImGui::Checkbox("##loop", &m_meta.looped);
|
||||||
|
ImGuiEx::Label("Volume");
|
||||||
|
ImGui::DragFloat("##vol", &m_meta.volume, 0.01f, 0, FLT_MAX);
|
||||||
|
|
||||||
auto* clip = static_cast<Clip*>(resources[0]);
|
auto* clip = static_cast<Clip*>(resources[0]);
|
||||||
ImGuiEx::Label("Length");
|
ImGuiEx::Label("Length");
|
||||||
ImGui::Text("%f", clip->getLengthSeconds());
|
ImGui::Text("%f", clip->getLengthSeconds());
|
||||||
|
@ -63,7 +104,7 @@ struct AssetBrowserPlugin final : AssetBrowser::IPlugin
|
||||||
|
|
||||||
if (m_playing_clip >= 0)
|
if (m_playing_clip >= 0)
|
||||||
{
|
{
|
||||||
if (ImGui::Button("Stop"))
|
if (ImGui::Button(ICON_FA_STOP "Stop"))
|
||||||
{
|
{
|
||||||
stopAudio();
|
stopAudio();
|
||||||
return;
|
return;
|
||||||
|
@ -76,7 +117,7 @@ struct AssetBrowserPlugin final : AssetBrowser::IPlugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_playing_clip < 0 && ImGui::Button("Play"))
|
if (m_playing_clip < 0 && ImGui::Button(ICON_FA_PLAY "Play"))
|
||||||
{
|
{
|
||||||
stopAudio();
|
stopAudio();
|
||||||
|
|
||||||
|
@ -85,6 +126,19 @@ struct AssetBrowserPlugin final : AssetBrowser::IPlugin
|
||||||
device.play(handle, true);
|
device.play(handle, true);
|
||||||
m_playing_clip = handle;
|
m_playing_clip = handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button(ICON_FA_CHECK "Apply")) {
|
||||||
|
const StaticString<512> src("volume = ", m_meta.volume
|
||||||
|
, "\nlooped = ", m_meta.looped ? "true" : "false"
|
||||||
|
);
|
||||||
|
AssetCompiler& compiler = m_app.getAssetCompiler();
|
||||||
|
compiler.updateMeta(resources[0]->getPath(), src);
|
||||||
|
if (compiler.compile(resources[0]->getPath())) {
|
||||||
|
resources[0]->getResourceManager().reload(*resources[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,119 +159,8 @@ struct AssetBrowserPlugin final : AssetBrowser::IPlugin
|
||||||
int m_playing_clip;
|
int m_playing_clip;
|
||||||
StudioApp& m_app;
|
StudioApp& m_app;
|
||||||
AssetBrowser& m_browser;
|
AssetBrowser& m_browser;
|
||||||
};
|
Meta m_meta;
|
||||||
|
u32 m_meta_res = 0;
|
||||||
|
|
||||||
struct ClipManagerUI final : StudioApp::GUIPlugin
|
|
||||||
{
|
|
||||||
explicit ClipManagerUI(StudioApp& app)
|
|
||||||
: m_app(app)
|
|
||||||
{
|
|
||||||
m_filter[0] = 0;
|
|
||||||
m_is_open = false;
|
|
||||||
m_toggle_ui.init("Clip manager", "Toggle clip manager", "clip_manager", "", true);
|
|
||||||
m_toggle_ui.func.bind<&ClipManagerUI::onAction>(this);
|
|
||||||
m_toggle_ui.is_selected.bind<&ClipManagerUI::isOpen>(this);
|
|
||||||
app.addWindowAction(&m_toggle_ui);
|
|
||||||
}
|
|
||||||
|
|
||||||
~ClipManagerUI() {
|
|
||||||
m_app.removeAction(&m_toggle_ui);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
/*
|
|
||||||
void pluginAdded(GUIPlugin& plugin) override
|
|
||||||
{
|
|
||||||
if (!equalStrings(plugin.getName(), "animation_editor")) return;
|
|
||||||
|
|
||||||
auto& anim_editor = (AnimEditor::IAnimationEditor&)plugin;
|
|
||||||
auto& event_type = anim_editor.createEventType("sound");
|
|
||||||
event_type.size = sizeof(SoundAnimationEvent);
|
|
||||||
event_type.label = "Sound";
|
|
||||||
event_type.editor.bind<ClipManagerUI, &ClipManagerUI::onSoundEventGUI>(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void onSoundEventGUI(u8* data, AnimEditor::Component& component) const
|
|
||||||
{
|
|
||||||
auto* ev = (SoundAnimationEvent*)data;
|
|
||||||
AudioScene* scene = (AudioScene*)m_app.getWorldEditor().getUniverse()->getScene(crc32("audio"));
|
|
||||||
auto getter = [](void* data, int idx, const char** out) -> bool {
|
|
||||||
auto* scene = (AudioScene*)data;
|
|
||||||
*out = scene->getClipName(idx);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
AudioScene::ClipInfo* clip = scene->getClipInfo(ev->clip);
|
|
||||||
int current = clip ? scene->getClipInfoIndex(clip) : -1;
|
|
||||||
|
|
||||||
if (ImGui::Combo("Clip", ¤t, getter, scene, scene->getClipCount()))
|
|
||||||
{
|
|
||||||
ev->clip = scene->getClipInfo(current)->name_hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const char* getName() const override { return "audio"; }
|
|
||||||
|
|
||||||
|
|
||||||
bool isOpen() const { return m_is_open; }
|
|
||||||
void onAction() { m_is_open = !m_is_open; }
|
|
||||||
|
|
||||||
|
|
||||||
void onWindowGUI() override
|
|
||||||
{
|
|
||||||
if (!m_is_open) return;
|
|
||||||
|
|
||||||
if (ImGui::Begin("Clip Manager", &m_is_open)) {
|
|
||||||
ImGui::SetNextItemWidth(-1);
|
|
||||||
ImGui::InputTextWithHint("##filter", "Filter", m_filter, sizeof(m_filter));
|
|
||||||
|
|
||||||
Universe* universe = m_app.getWorldEditor().getUniverse();
|
|
||||||
auto* audio_scene = static_cast<AudioScene*>(universe->getScene(crc32("audio")));
|
|
||||||
u32 clip_count = audio_scene->getClipCount();
|
|
||||||
for (u32 clip_id = 0; clip_id < clip_count; ++clip_id) {
|
|
||||||
AudioScene::ClipInfo* clip_info = audio_scene->getClipInfoByIndex(clip_id);
|
|
||||||
if (!clip_info) continue;
|
|
||||||
|
|
||||||
if (m_filter[0] != 0 && stristr(clip_info->name, m_filter) == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::TreeNode((const void*)(uintptr)clip_id, "%s", clip_info->name)) {
|
|
||||||
if (ImGui::InputText("Name", clip_info->name, sizeof(clip_info->name))) {
|
|
||||||
clip_info->name_hash = crc32(clip_info->name);
|
|
||||||
}
|
|
||||||
char path[MAX_PATH_LENGTH];
|
|
||||||
copyString(path, clip_info->clip ? clip_info->clip->getPath().c_str() : "");
|
|
||||||
ImGuiEx::Label("Clip");
|
|
||||||
if (m_app.getAssetBrowser().resourceInput("clip", Span(path), Clip::TYPE)) {
|
|
||||||
audio_scene->setClip(clip_id, Path(path));
|
|
||||||
}
|
|
||||||
ImGuiEx::Label("Volume");
|
|
||||||
ImGui::InputFloat("##volume", &clip_info->volume);
|
|
||||||
ImGuiEx::Label("Looped");
|
|
||||||
ImGui::Checkbox("##looped", &clip_info->looped);
|
|
||||||
if (ImGui::Button("Remove")) {
|
|
||||||
audio_scene->removeClip(clip_info);
|
|
||||||
--clip_count;
|
|
||||||
}
|
|
||||||
ImGui::TreePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::Button("Add")) {
|
|
||||||
audio_scene->addClip("test", Path("test.ogg"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::End();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
StudioApp& m_app;
|
|
||||||
char m_filter[256];
|
|
||||||
bool m_is_open;
|
|
||||||
Action m_toggle_ui;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -226,7 +169,6 @@ struct StudioAppPlugin : StudioApp::IPlugin
|
||||||
explicit StudioAppPlugin(StudioApp& app)
|
explicit StudioAppPlugin(StudioApp& app)
|
||||||
: m_app(app)
|
: m_app(app)
|
||||||
, m_asset_browser_plugin(app)
|
, m_asset_browser_plugin(app)
|
||||||
, m_clip_manager_ui(app)
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
const char* getName() const override { return "audio"; }
|
const char* getName() const override { return "audio"; }
|
||||||
|
@ -241,8 +183,8 @@ struct StudioAppPlugin : StudioApp::IPlugin
|
||||||
IAllocator& allocator = m_app.getAllocator();
|
IAllocator& allocator = m_app.getAllocator();
|
||||||
|
|
||||||
m_app.getAssetBrowser().addPlugin(m_asset_browser_plugin);
|
m_app.getAssetBrowser().addPlugin(m_asset_browser_plugin);
|
||||||
|
const char* extensions[] = { "ogg", nullptr };
|
||||||
m_app.addPlugin(m_clip_manager_ui);
|
m_app.getAssetCompiler().addPlugin(m_asset_browser_plugin, extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -280,13 +222,12 @@ struct StudioAppPlugin : StudioApp::IPlugin
|
||||||
~StudioAppPlugin()
|
~StudioAppPlugin()
|
||||||
{
|
{
|
||||||
m_app.getAssetBrowser().removePlugin(m_asset_browser_plugin);
|
m_app.getAssetBrowser().removePlugin(m_asset_browser_plugin);
|
||||||
m_app.removePlugin(m_clip_manager_ui);
|
m_app.getAssetCompiler().removePlugin(m_asset_browser_plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
StudioApp& m_app;
|
StudioApp& m_app;
|
||||||
AssetBrowserPlugin m_asset_browser_plugin;
|
AssetBrowserPlugin m_asset_browser_plugin;
|
||||||
ClipManagerUI m_clip_manager_ui;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -660,12 +660,14 @@ inline Variant::Type _getVariantType(VariantTag<EntityPtr>) { return Variant::EN
|
||||||
inline Variant::Type _getVariantType(VariantTag<EntityRef>) { return Variant::ENTITY; }
|
inline Variant::Type _getVariantType(VariantTag<EntityRef>) { return Variant::ENTITY; }
|
||||||
inline Variant::Type _getVariantType(VariantTag<Vec2>) { return Variant::VEC2; }
|
inline Variant::Type _getVariantType(VariantTag<Vec2>) { return Variant::VEC2; }
|
||||||
inline Variant::Type _getVariantType(VariantTag<Vec3>) { return Variant::VEC3; }
|
inline Variant::Type _getVariantType(VariantTag<Vec3>) { return Variant::VEC3; }
|
||||||
|
inline Variant::Type _getVariantType(VariantTag<Path>) { return Variant::CSTR; }
|
||||||
inline Variant::Type _getVariantType(VariantTag<DVec3>) { return Variant::DVEC3; }
|
inline Variant::Type _getVariantType(VariantTag<DVec3>) { return Variant::DVEC3; }
|
||||||
template <typename T> inline Variant::Type getVariantType() { return _getVariantType(VariantTag<RemoveCVR<T>>{}); }
|
template <typename T> inline Variant::Type getVariantType() { return _getVariantType(VariantTag<RemoveCVR<T>>{}); }
|
||||||
|
|
||||||
inline bool fromVariant(int i, Span<Variant> args, VariantTag<bool>) { return args[i].b; }
|
inline bool fromVariant(int i, Span<Variant> args, VariantTag<bool>) { return args[i].b; }
|
||||||
inline float fromVariant(int i, Span<Variant> args, VariantTag<float>) { return args[i].f; }
|
inline float fromVariant(int i, Span<Variant> args, VariantTag<float>) { return args[i].f; }
|
||||||
inline const char* fromVariant(int i, Span<Variant> args, VariantTag<const char*>) { return args[i].s; }
|
inline const char* fromVariant(int i, Span<Variant> args, VariantTag<const char*>) { return args[i].s; }
|
||||||
|
inline Path fromVariant(int i, Span<Variant> args, VariantTag<Path>) { return Path(args[i].s); }
|
||||||
inline i32 fromVariant(int i, Span<Variant> args, VariantTag<i32>) { return args[i].i; }
|
inline i32 fromVariant(int i, Span<Variant> args, VariantTag<i32>) { return args[i].i; }
|
||||||
inline u32 fromVariant(int i, Span<Variant> args, VariantTag<u32>) { return args[i].u; }
|
inline u32 fromVariant(int i, Span<Variant> args, VariantTag<u32>) { return args[i].u; }
|
||||||
inline Vec2 fromVariant(int i, Span<Variant> args, VariantTag<Vec2>) { return args[i].v2; }
|
inline Vec2 fromVariant(int i, Span<Variant> args, VariantTag<Vec2>) { return args[i].v2; }
|
||||||
|
|
Loading…
Reference in a new issue