mirror of
https://github.com/harfang3d/harfang3d.git
synced 2024-06-16 04:02:14 +00:00
commit
e57798947d
|
@ -971,8 +971,8 @@ def bind_scene(gen):
|
|||
|
||||
gen.bind_method(rigid_body, 'GetLinearDamping', 'float', [])
|
||||
gen.bind_method(rigid_body, 'SetLinearDamping', 'void', ['float damping'])
|
||||
gen.bind_method(rigid_body, 'GetAngularDamping', 'hg::Vec3', [])
|
||||
gen.bind_method(rigid_body, 'SetAngularDamping', 'void', ['const hg::Vec3 &damping'])
|
||||
gen.bind_method(rigid_body, 'GetAngularDamping', 'float', [])
|
||||
gen.bind_method(rigid_body, 'SetAngularDamping', 'void', ['float damping'])
|
||||
gen.bind_method(rigid_body, 'GetRestitution', 'float', [])
|
||||
gen.bind_method(rigid_body, 'SetRestitution', 'void', ['float restitution'])
|
||||
gen.bind_method(rigid_body, 'GetFriction', 'float', [])
|
||||
|
@ -2132,6 +2132,7 @@ static void _SetViewTransform(bgfx::ViewId view_id, const hg::Mat4 &view, const
|
|||
('TF_SamplerMagAnisotropic', 'BGFX_SAMPLER_MAG_ANISOTROPIC'),
|
||||
('TF_BlitDestination', 'BGFX_TEXTURE_BLIT_DST'),
|
||||
('TF_ReadBack', 'BGFX_TEXTURE_READ_BACK'),
|
||||
('TF_RenderTarget', 'BGFX_TEXTURE_RT')
|
||||
], 'TextureFlags')
|
||||
|
||||
gen.bind_function('hg::LoadTextureFlagsFromFile', 'uint64_t', ['const std::string &path'], {'rval_constants_group': 'TextureFlags'})
|
||||
|
@ -2430,10 +2431,11 @@ static void _FrameBuffer_GetTextures(hg::FrameBuffer &framebuffer, hg::Texture &
|
|||
color = hg::GetColorTexture(framebuffer);
|
||||
depth = hg::GetDepthTexture(framebuffer);
|
||||
}
|
||||
static bool _IsValid(const hg::FrameBuffer &fb) { return bgfx::isValid(fb.handle); }
|
||||
''')
|
||||
gen.bind_function('GetTextures', 'void', ['hg::FrameBuffer &framebuffer', 'hg::Texture &color', 'hg::Texture &depth'], {'route': route_lambda('_FrameBuffer_GetTextures'), 'arg_out': ['color', 'depth']})
|
||||
gen.bind_function('hg::DestroyFrameBuffer', 'void', ['hg::FrameBuffer &frameBuffer'])
|
||||
|
||||
gen.bind_function('hg::IsValid', 'bool', ['const hg::FrameBuffer &fb'], {'route': route_lambda('_IsValid')})
|
||||
#
|
||||
vertices = gen.begin_class('hg::Vertices')
|
||||
gen.bind_constructor(vertices, ['const bgfx::VertexLayout &decl', 'size_t count'])
|
||||
|
@ -4166,8 +4168,9 @@ static hg::SpatializedSourceState *__ConstructSpatializedSourceState(hg::Mat4 mt
|
|||
gen.bind_function('hg::StreamWAVFileSpatialized', 'hg::SourceRef', ['const char *path', 'const hg::SpatializedSourceState &state'], {'rval_constants_group': 'SourceRef'})
|
||||
gen.bind_function('hg::StreamWAVAssetSpatialized', 'hg::SourceRef', ['const char *name', 'const hg::SpatializedSourceState &state'], {'rval_constants_group': 'SourceRef'})
|
||||
|
||||
gen.bind_function('hg::GetSourceDuration', 'hg::time_ns', ['hg::SourceRef source'], {'constants_group': {'source': 'SourceRef'}})
|
||||
gen.bind_function('hg::GetSourceTimecode', 'hg::time_ns', ['hg::SourceRef source'], {'constants_group': {'source': 'SourceRef'}})
|
||||
# [unimplemented] gen.bind_function('hg::SetSourceStreamTimecode', 'bool', ['hg::SourceRef source', 'hg::time_ns t'])
|
||||
gen.bind_function('hg::SetSourceTimecode', 'bool', ['hg::SourceRef source', 'hg::time_ns t'], {'constants_group': {'source': 'SourceRef'}})
|
||||
|
||||
gen.bind_function('hg::SetSourceVolume', 'void', ['hg::SourceRef source', 'float volume'], {'constants_group': {'source': 'SourceRef'}})
|
||||
gen.bind_function('hg::SetSourcePanning', 'void', ['hg::SourceRef source', 'float panning'], {'constants_group': {'source': 'SourceRef'}})
|
||||
|
|
1
doc/doc/GetSourceDuration.md
Normal file
1
doc/doc/GetSourceDuration.md
Normal file
|
@ -0,0 +1 @@
|
|||
Return the duration of an audio source.
|
1
doc/doc/SetSourceTimecode.md
Normal file
1
doc/doc/SetSourceTimecode.md
Normal file
|
@ -0,0 +1 @@
|
|||
Set timecode of the audio source.
|
|
@ -1,22 +1,22 @@
|
|||
.title Resources & Assets
|
||||
|
||||
By convention, production files are called: **resources** (eg. *the project resources*).
|
||||
By convention, production files are called **resources** (eg. *the project resources*).
|
||||
|
||||
Files issued from the compilation of production files for a specific target are called: **assets** (eg. *the project assets for Windows PC*).
|
||||
Files issued from the compilation of production files for a specific target are called **assets** (eg. *the project assets for Windows PC*).
|
||||
|
||||
[TOC]
|
||||
|
||||
## Resource Formats
|
||||
|
||||
During development resources are stored in *production formats*, these formats are meant for efficient editing. Before they can be loaded at runtime, resources must be compiled into their *runtime formats* as assets which are specific to the target platform.
|
||||
During development resources are stored in *production formats*, these formats are meant for editing. Before they can be loaded at runtime, resources must be compiled into their *runtime formats* as assets specific to the target platform.
|
||||
|
||||
To compile a project resources use:
|
||||
|
||||
```text
|
||||
assetc <input_resources_folder> [<output_assets_folder>]
|
||||
assetc <project_resources_folder> [<optional_project_assets_folder>]
|
||||
```
|
||||
|
||||
If no output folder is specified the input folder suffixed with *_compiled* is implied as output.
|
||||
If no output folder is specified the input folder suffixed with *_compiled* is implied.
|
||||
|
||||
Upon execution the assets compiler will scan the input folder and compile all supported resource types to assets. Unsupported resources are copied untransformed to the output folder.
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ Harfang for Python is distributed as a wheel package `(.whl)` compatible with of
|
|||
|
||||
## Installation
|
||||
|
||||
* On Windows, Install from the command line by typing `pip install harfang`.
|
||||
* On Windows, install from the command line by typing `pip install harfang`.
|
||||
* On Linux, download the wheel for your distribution then install from a terminal by typing `pip install /path/to/harfang.whl`.
|
||||
|
||||
## Testing your Installation
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
Harfang is a high-level software library to create applications that display 2D/3D visuals and play sound/music.
|
||||
|
||||
It provides a unified API to write programs using different programming languages and is available for Windows and Linux (for more details, please refer to the [man.Requirements]).
|
||||
It provides a unified API to write programs using different programming languages and is available for Windows and Linux. For more details, see [man.Requirements].
|
||||
|
||||
## Supported Programming Languages
|
||||
|
||||
For installation instructions for each supported language please refer to the corresponding manual page:
|
||||
The installation instructions for each supported language are found in the following manual pages:
|
||||
|
||||
* [man.CPython]
|
||||
* [man.Lua]
|
||||
|
|
|
@ -96,7 +96,18 @@ static bool CheckALSuccess(const char *file = "unknown", ALuint line = 0) {
|
|||
} while (0);
|
||||
|
||||
//
|
||||
static const ALenum AFF_ALFormat[AFF_Count] = {AL_FORMAT_MONO16, AL_FORMAT_MONO16, AL_FORMAT_STEREO16, AL_FORMAT_STEREO16};
|
||||
static inline int AFF_ALFormat(AudioFrameFormat fmt) {
|
||||
switch (fmt) {
|
||||
case AFF_LPCM_44KHZ_S16_Mono:
|
||||
case AFF_LPCM_48KHZ_S16_Mono:
|
||||
return AL_FORMAT_MONO16;
|
||||
case AFF_LPCM_44KHZ_S16_Stereo:
|
||||
case AFF_LPCM_48KHZ_S16_Stereo:
|
||||
return AL_FORMAT_STEREO16;
|
||||
default:
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
static void AllocStream(ALStream &stream) {
|
||||
|
@ -172,16 +183,19 @@ static bool UpdateSourceStream(SourceRef src_ref) {
|
|||
stream.buffers_format[stream.put] = pcm_format;
|
||||
|
||||
__AL_CALL(
|
||||
alBufferData(stream.buffers[stream.put], AFF_ALFormat[pcm_format], pcm_buffer, numeric_cast<ALsizei>(pcm_size), AFF_Frequency[pcm_format]));
|
||||
alBufferData(stream.buffers[stream.put], AFF_ALFormat(pcm_format), pcm_buffer, numeric_cast<ALsizei>(pcm_size), AFF_Frequency[pcm_format]));
|
||||
__AL_CALL(alSourceQueueBuffers(src, 1, &stream.buffers[stream.put]));
|
||||
|
||||
stream.put = (stream.put + 1) % stream.buffers.size();
|
||||
--stream.free_buffer_count;
|
||||
} else {
|
||||
if (stream.streamer.IsEnded(stream.ref)) { // stream is EOF
|
||||
if (stream.loop) // handle loop
|
||||
if (stream.loop) { // handle loop
|
||||
if (!stream.streamer.Seek(stream.ref, 0)) // FIXME seek to sample_start
|
||||
return false;
|
||||
} else {
|
||||
break; // EOF
|
||||
}
|
||||
} else {
|
||||
return false; // stream error
|
||||
}
|
||||
|
@ -347,7 +361,7 @@ static SoundRef LoadSound(IAudioStreamer streamer, const char *path) {
|
|||
sound.buffers.push_back(AL_INVALID_VALUE);
|
||||
__AL_CALL(alGenBuffers(1, &sound.buffers.back()));
|
||||
__AL_CALL(alBufferData(
|
||||
sound.buffers.back(), AFF_ALFormat[pcm_format], (const ALvoid *)pcm_buffer, numeric_cast<ALsizei>(pcm_size), AFF_Frequency[pcm_format]));
|
||||
sound.buffers.back(), AFF_ALFormat(pcm_format), (const ALvoid *)pcm_buffer, numeric_cast<ALsizei>(pcm_size), AFF_Frequency[pcm_format]));
|
||||
}
|
||||
|
||||
streamer.Close(stream_ref);
|
||||
|
@ -463,7 +477,6 @@ time_ns GetSourceDuration(SourceRef src_ref) {
|
|||
if (src_ref < 0 || src_ref >= max_source)
|
||||
return 0;
|
||||
|
||||
std::lock_guard<std::mutex> lock(al_mixer.lock);
|
||||
ALStream &stream = al_mixer.streams[src_ref];
|
||||
if (stream.ref == InvalidAudioStreamRef)
|
||||
return 0;
|
||||
|
@ -472,12 +485,21 @@ time_ns GetSourceDuration(SourceRef src_ref) {
|
|||
return stream.streamer.GetDuration(stream.ref);
|
||||
}
|
||||
|
||||
#if 0
|
||||
// [todo]
|
||||
bool SetSourceStreamTimecode(SourceRef src_ref, time_ns t) {
|
||||
return false;
|
||||
bool SetSourceTimecode(SourceRef src_ref, time_ns t) {
|
||||
std::lock_guard<std::mutex> mixer_lock(al_mixer.lock);
|
||||
if (src_ref < 0 || src_ref >= max_source)
|
||||
return false;
|
||||
|
||||
ALStream &stream = al_mixer.streams[src_ref];
|
||||
if (stream.ref == InvalidAudioStreamRef)
|
||||
return 0;
|
||||
|
||||
if (stream.streamer.Seek(stream.ref, t) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void SetSourceVolume(SourceRef src_ref, float volume) {
|
||||
if (src_ref < 0 || src_ref >= max_source)
|
||||
|
@ -546,6 +568,8 @@ SourceState GetSourceState(SourceRef src_ref) {
|
|||
return SS_Playing;
|
||||
if (state == AL_PAUSED)
|
||||
return SS_Paused;
|
||||
if (state == AL_STOPPED)
|
||||
return SS_Stopped;
|
||||
|
||||
return SS_Invalid;
|
||||
}
|
||||
|
@ -570,7 +594,6 @@ void StopSource(SourceRef src_ref) {
|
|||
|
||||
__AL_CALL(alSourceStop(src));
|
||||
__AL_CALL(alSourcei(src, AL_BUFFER, 0));
|
||||
__AL_CALL(alSourceRewind(src));
|
||||
|
||||
auto &stream = al_mixer.streams[src_ref];
|
||||
|
||||
|
|
|
@ -43,14 +43,14 @@ static const SoundRef InvalidSoundRef = -1;
|
|||
SoundRef LoadWAVSoundFile(const char *path);
|
||||
SoundRef LoadWAVSoundAsset(const char *name);
|
||||
|
||||
void UnloadSound(SoundRef snd);
|
||||
void UnloadSound(SoundRef snd_ref);
|
||||
|
||||
//
|
||||
using SourceRef = int;
|
||||
static const SourceRef InvalidSourceRef = -1;
|
||||
|
||||
SourceRef PlayStereo(SoundRef snd, const StereoSourceState &state);
|
||||
SourceRef PlaySpatialized(SoundRef snd, const SpatializedSourceState &state);
|
||||
SourceRef PlayStereo(SoundRef snd_ref, const StereoSourceState &state);
|
||||
SourceRef PlaySpatialized(SoundRef snd_ref, const SpatializedSourceState &state);
|
||||
|
||||
SourceRef StreamWAVFileStereo(const char *path, const StereoSourceState &state);
|
||||
SourceRef StreamWAVAssetStereo(const char *name, const StereoSourceState &state);
|
||||
|
@ -58,24 +58,24 @@ SourceRef StreamWAVFileSpatialized(const char *path, const SpatializedSourceStat
|
|||
SourceRef StreamWAVAssetSpatialized(const char *name, const SpatializedSourceState &state);
|
||||
|
||||
//
|
||||
// time_ns GetSourceDuration(SourceRef source);
|
||||
time_ns GetSourceTimecode(SourceRef src);
|
||||
// [unimplemented] bool SetSourceStreamTimecode(SourceRef source, time_ns t);
|
||||
time_ns GetSourceDuration(SourceRef src_ref);
|
||||
time_ns GetSourceTimecode(SourceRef src_ref);
|
||||
bool SetSourceTimecode(SourceRef src_ref, time_ns t);
|
||||
|
||||
//
|
||||
void SetSourceVolume(SourceRef src, float volume);
|
||||
void SetSourcePanning(SourceRef src, float panning);
|
||||
void SetSourceRepeat(SourceRef src, SourceRepeat repeat);
|
||||
void SetSourceTransform(SourceRef src, const hg::Mat4 &world, const hg::Vec3 &velocity);
|
||||
void SetSourceVolume(SourceRef src_ref, float volume);
|
||||
void SetSourcePanning(SourceRef src_ref, float panning);
|
||||
void SetSourceRepeat(SourceRef src_ref, SourceRepeat repeat);
|
||||
void SetSourceTransform(SourceRef src_ref, const hg::Mat4 &world, const hg::Vec3 &velocity);
|
||||
|
||||
//
|
||||
enum SourceState { SS_Initial, SS_Playing, SS_Paused, SS_Stopped, SS_Invalid };
|
||||
|
||||
SourceState GetSourceState(SourceRef src);
|
||||
SourceState GetSourceState(SourceRef src_ref);
|
||||
|
||||
void PauseSource(SourceRef src);
|
||||
void PauseSource(SourceRef src_ref);
|
||||
|
||||
void StopSource(SourceRef src);
|
||||
void StopSource(SourceRef src_ref);
|
||||
void StopAllSources();
|
||||
|
||||
// [todo] loop point?
|
||||
|
|
|
@ -867,9 +867,9 @@ void RigidBody::SetLinearDamping(float damping) {
|
|||
scene_ref->scene->SetRigidBodyLinearDamping(ref, damping);
|
||||
}
|
||||
|
||||
Vec3 RigidBody::GetAngularDamping() const { return scene_ref && scene_ref->scene ? scene_ref->scene->GetRigidBodyAngularDamping(ref) : Vec3{}; }
|
||||
float RigidBody::GetAngularDamping() const { return scene_ref && scene_ref->scene ? scene_ref->scene->GetRigidBodyAngularDamping(ref) : 0.f; }
|
||||
|
||||
void RigidBody::SetAngularDamping(const Vec3 &damping) {
|
||||
void RigidBody::SetAngularDamping(float damping) {
|
||||
if (scene_ref && scene_ref->scene)
|
||||
scene_ref->scene->SetRigidBodyAngularDamping(ref, damping);
|
||||
}
|
||||
|
|
|
@ -180,8 +180,8 @@ struct RigidBody { // 16B on 64 bit
|
|||
|
||||
float GetLinearDamping() const;
|
||||
void SetLinearDamping(float damping);
|
||||
Vec3 GetAngularDamping() const;
|
||||
void SetAngularDamping(const Vec3 &damping);
|
||||
float GetAngularDamping() const;
|
||||
void SetAngularDamping(float damping);
|
||||
|
||||
float GetRestitution() const;
|
||||
void SetRestitution(float restitution);
|
||||
|
|
|
@ -832,15 +832,15 @@ void Scene::SetRigidBodyLinearDamping(ComponentRef ref, float damping) {
|
|||
rb->linear_damping = pack_float<uint8_t>(damping);
|
||||
}
|
||||
|
||||
Vec3 Scene::GetRigidBodyAngularDamping(ComponentRef ref) const {
|
||||
float Scene::GetRigidBodyAngularDamping(ComponentRef ref) const {
|
||||
if (auto rb = GetComponent_(rigid_bodies, ref))
|
||||
return {unpack_float<uint8_t>(rb->angular_damping[0]), unpack_float<uint8_t>(rb->angular_damping[1]), unpack_float<uint8_t>(rb->angular_damping[2])};
|
||||
return unpack_float<uint8_t>(rb->angular_damping);
|
||||
return {};
|
||||
}
|
||||
|
||||
void Scene::SetRigidBodyAngularDamping(ComponentRef ref, const Vec3 &damping) {
|
||||
void Scene::SetRigidBodyAngularDamping(ComponentRef ref, float damping) {
|
||||
if (auto rb = GetComponent_(rigid_bodies, ref))
|
||||
rb->angular_damping = {pack_float<uint8_t>(damping.x), pack_float<uint8_t>(damping.y), pack_float<uint8_t>(damping.z)};
|
||||
rb->angular_damping = pack_float<uint8_t>(damping);
|
||||
}
|
||||
|
||||
float Scene::GetRigidBodyRestitution(ComponentRef ref) const {
|
||||
|
|
|
@ -407,8 +407,8 @@ public:
|
|||
|
||||
float GetRigidBodyLinearDamping(ComponentRef ref) const;
|
||||
void SetRigidBodyLinearDamping(ComponentRef ref, float damping);
|
||||
Vec3 GetRigidBodyAngularDamping(ComponentRef ref) const;
|
||||
void SetRigidBodyAngularDamping(ComponentRef ref, const Vec3 &damping);
|
||||
float GetRigidBodyAngularDamping(ComponentRef ref) const;
|
||||
void SetRigidBodyAngularDamping(ComponentRef ref, float damping);
|
||||
|
||||
float GetRigidBodyRestitution(ComponentRef ref) const;
|
||||
void SetRigidBodyRestitution(ComponentRef ref, float restitution);
|
||||
|
@ -730,8 +730,8 @@ private:
|
|||
Mat4 prv{Mat4::Identity}, cur{Mat4::Identity};
|
||||
Vec3 scl{Vec3::One};
|
||||
|
||||
uint8_t linear_damping{pack_float<uint8_t>(1.f)};
|
||||
std::array<uint8_t, 3> angular_damping = {pack_float<uint8_t>(1.f), pack_float<uint8_t>(1.f), pack_float<uint8_t>(1.f)};
|
||||
uint8_t linear_damping{pack_float<uint8_t>(0.f)};
|
||||
uint8_t angular_damping{pack_float<uint8_t>(0.f)};
|
||||
|
||||
float restitution{0.f}, friction{0.5f}, rolling_friction{0.f};
|
||||
};
|
||||
|
@ -785,7 +785,7 @@ private:
|
|||
friend void LoadComponent(Object_ *data_, const Reader &ir, const Handle &h, const Reader &deps_ir, const ReadProvider &deps_ip,
|
||||
PipelineResources &resources, const PipelineInfo &pipeline, bool queue_texture_loads, bool do_not_load_resources, uint32_t version);
|
||||
friend void LoadComponent(Light_ *data_, const Reader &ir, const Handle &h);
|
||||
friend void LoadComponent(RigidBody_ *data_, const Reader &ir, const Handle &h);
|
||||
friend void LoadComponent(RigidBody_ *data_, const Reader &ir, const Handle &h, uint32_t version);
|
||||
friend void LoadComponent(Collision_ *data_, const Reader &ir, const Handle &h);
|
||||
friend void LoadComponent(Instance_ *data_, const Reader &ir, const Handle &h);
|
||||
friend void LoadComponent(Script_ *data_, const Reader &ir, const Handle &h);
|
||||
|
|
|
@ -286,25 +286,27 @@ void SceneBullet3Physics::NodeCreatePhysics(const Node &node, const Reader &ir,
|
|||
rb_info.m_friction = rb.GetFriction();
|
||||
rb_info.m_rollingFriction = rb.GetRollingFriction();
|
||||
rb_info.m_startWorldTransform = bt_trs;
|
||||
rb_info.m_linearDamping = rb.GetLinearDamping();
|
||||
rb_info.m_angularDamping = rb.GetAngularDamping();
|
||||
|
||||
_node.body = new btRigidBody(rb_info);
|
||||
|
||||
_node.body->setCollisionShape(root_shape);
|
||||
_node.body->setUserIndex(node.ref.idx); // ref back to node
|
||||
|
||||
world->addRigidBody(_node.body);
|
||||
|
||||
// configure
|
||||
const auto type = rb.GetType();
|
||||
const auto flags = _node.body->getCollisionFlags();
|
||||
|
||||
if (type == RBT_Dynamic) {
|
||||
if (type == RBT_Dynamic)
|
||||
_node.body->setCollisionFlags(flags & ~(btRigidBody::CF_KINEMATIC_OBJECT | btCollisionObject::CF_STATIC_OBJECT));
|
||||
} else if (type == RBT_Kinematic) {
|
||||
else if (type == RBT_Kinematic)
|
||||
_node.body->setCollisionFlags((flags | btRigidBody::CF_KINEMATIC_OBJECT) & ~btCollisionObject::CF_STATIC_OBJECT);
|
||||
} else {
|
||||
else
|
||||
_node.body->setCollisionFlags((flags & ~btRigidBody::CF_KINEMATIC_OBJECT) | btCollisionObject::CF_STATIC_OBJECT);
|
||||
}
|
||||
|
||||
// add to world
|
||||
world->addRigidBody(_node.body);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -426,9 +428,9 @@ void SceneBullet3Physics::SyncBodiesFromScene(const Scene &scene) {
|
|||
const auto flags = body->getCollisionFlags();
|
||||
|
||||
if (flags == btRigidBody::CF_KINEMATIC_OBJECT) {
|
||||
const auto world = hg::Orthonormalize(trs.GetWorld());
|
||||
// const auto world = TransformationMat4(GetT(w), GetR(w), Vec3::One);
|
||||
body->setWorldTransform(to_btTransform(world));
|
||||
const auto w = trs.GetWorld();
|
||||
const auto w2 = TransformationMat4(GetT(w), GetR(w), Vec3::One);
|
||||
body->setWorldTransform(to_btTransform(w2));
|
||||
} else if (flags == btRigidBody::CF_DYNAMIC_OBJECT) {
|
||||
const auto world = body->getInterpolationWorldTransform();
|
||||
trs.SetWorld(from_btTransform(world));
|
||||
|
|
|
@ -57,6 +57,12 @@ void SaveComponent(const Scene::Light_ *data_, const Writer &iw, const Handle &h
|
|||
void SaveComponent(const Scene::RigidBody_ *data_, const Writer &iw, const Handle &h) {
|
||||
Write(iw, h, data_->type);
|
||||
iw.write(h, &data_->cur.m[0][0], sizeof(float) * 4 * 3);
|
||||
Write(iw, h, data_->scl);
|
||||
Write(iw, h, data_->linear_damping);
|
||||
Write(iw, h, data_->angular_damping);
|
||||
Write(iw, h, data_->restitution);
|
||||
Write(iw, h, data_->friction);
|
||||
Write(iw, h, data_->rolling_friction);
|
||||
}
|
||||
|
||||
void SaveComponent(const Scene::Script_ *data_, const Writer &iw, const Handle &h) {
|
||||
|
@ -143,9 +149,18 @@ void LoadComponent(Scene::Light_ *data_, const Reader &ir, const Handle &h) {
|
|||
Read(ir, h, data_->shadow_bias);
|
||||
}
|
||||
|
||||
void LoadComponent(Scene::RigidBody_ *data_, const Reader &ir, const Handle &h) {
|
||||
void LoadComponent(Scene::RigidBody_ *data_, const Reader &ir, const Handle &h, uint32_t version) {
|
||||
Read(ir, h, data_->type);
|
||||
ir.read(h, &data_->cur.m[0][0], sizeof(float) * 4 * 3);
|
||||
|
||||
if (version >= 5) {
|
||||
Read(ir, h, data_->scl);
|
||||
Read(ir, h, data_->linear_damping);
|
||||
Read(ir, h, data_->angular_damping);
|
||||
Read(ir, h, data_->restitution);
|
||||
Read(ir, h, data_->friction);
|
||||
Read(ir, h, data_->rolling_friction);
|
||||
}
|
||||
}
|
||||
|
||||
void LoadComponent(Scene::Script_ *data_, const Reader &ir, const Handle &h) {
|
||||
|
@ -179,7 +194,7 @@ void LoadComponent(Scene::Instance_ *data_, const Reader &ir, const Handle &h) {
|
|||
}
|
||||
|
||||
//
|
||||
uint32_t GetSceneBinaryFormatVersion() { return 4; }
|
||||
uint32_t GetSceneBinaryFormatVersion() { return 5; }
|
||||
|
||||
bool Scene::Save_binary(
|
||||
const Writer &iw, const Handle &h, const PipelineResources &resources, uint32_t save_flags, const std::vector<NodeRef> *nodes_to_save) const {
|
||||
|
@ -195,6 +210,7 @@ bool Scene::Save_binary(
|
|||
version 2: add skinning support in Object component
|
||||
version 3: add support for arbitrary number of bones
|
||||
version 4: light intensity factors
|
||||
version 5: save rigid body properties
|
||||
*/
|
||||
const auto version = GetSceneBinaryFormatVersion();
|
||||
Write<uint32_t>(iw, h, version);
|
||||
|
@ -520,7 +536,7 @@ bool Scene::Load_binary(const Reader &ir, const Handle &h, const char *name, con
|
|||
rigid_body_refs.resize(rigid_body_count);
|
||||
for (size_t i = 0; i < rigid_body_count; ++i) {
|
||||
const auto ref = rigid_body_refs[i] = CreateRigidBody().ref;
|
||||
LoadComponent(&rigid_bodies[ref.idx], ir, h);
|
||||
LoadComponent(&rigid_bodies[ref.idx], ir, h, version);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -71,6 +71,12 @@ void SaveComponent(const Scene::Light_ *data_, json &js) {
|
|||
void SaveComponent(const Scene::RigidBody_ *data_, json &js) {
|
||||
js["type"] = data_->type;
|
||||
js["world"] = data_->cur;
|
||||
js["scale"] = data_->scl;
|
||||
js["linear_damping"] = data_->linear_damping;
|
||||
js["angular_damping"] = data_->angular_damping;
|
||||
js["restitution"] = data_->restitution;
|
||||
js["friction"] = data_->friction;
|
||||
js["rolling_friction"] = data_->rolling_friction;
|
||||
}
|
||||
|
||||
void SaveComponent(const Scene::Script_ *data_, json &js) {
|
||||
|
@ -186,6 +192,18 @@ void LoadComponent(Scene::Light_ *data_, const json &js) {
|
|||
void LoadComponent(Scene::RigidBody_ *data_, const json &js) {
|
||||
data_->type = js.at("type");
|
||||
data_->cur = js.at("world");
|
||||
if (js.find("scale") != std::end(js))
|
||||
data_->scl = js.at("scale");
|
||||
if (js.find("linear_damping") != std::end(js))
|
||||
data_->linear_damping = js.at("linear_damping");
|
||||
if (js.find("angular_damping") != std::end(js))
|
||||
data_->angular_damping = js.at("angular_damping");
|
||||
if (js.find("restitution") != std::end(js))
|
||||
data_->restitution = js.at("restitution");
|
||||
if (js.find("friction") != std::end(js))
|
||||
data_->friction = js.at("friction");
|
||||
if (js.find("rolling_friction") != std::end(js))
|
||||
data_->rolling_friction = js.at("rolling_friction");
|
||||
}
|
||||
|
||||
void LoadComponent(Scene::Script_ *data_, const json &js) {
|
||||
|
|
|
@ -221,8 +221,9 @@ static int WavAudioStreamGetFrame(AudioStreamRef ref, uintptr_t *data, int *size
|
|||
|
||||
const auto data_cursor = stream.reader->tell(stream.handle) - stream.data_offset; // position in data chunk
|
||||
const auto data_left = stream.data_size - data_cursor; // data left in chunk
|
||||
if (data_left == 0)
|
||||
if (data_left == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto data_to_read = Min(data_left, stream.frame.capacity()); // amount of data to read
|
||||
if (stream.reader->read(stream.handle, stream.frame.data(), data_to_read) != data_to_read)
|
||||
|
|
|
@ -13,37 +13,109 @@
|
|||
|
||||
using namespace hg;
|
||||
|
||||
TEST(Audio, InitShutdown) {
|
||||
start_timer();
|
||||
class Audio : public ::testing::Test {
|
||||
protected:
|
||||
bool initialized;
|
||||
|
||||
EXPECT_TRUE(AudioInit());
|
||||
AudioShutdown();
|
||||
void SetUp() override {
|
||||
start_timer();
|
||||
initialized = AudioInit();
|
||||
}
|
||||
|
||||
stop_timer();
|
||||
}
|
||||
void TearDown() override {
|
||||
AudioShutdown();
|
||||
stop_timer();
|
||||
}
|
||||
};
|
||||
|
||||
TEST(Audio, PlayWAV) {
|
||||
EXPECT_TRUE(AudioInit());
|
||||
TEST_F(Audio, InitShutdown) { EXPECT_TRUE(initialized); }
|
||||
|
||||
TEST_F(Audio, PlayWAV) {
|
||||
EXPECT_TRUE(initialized);
|
||||
|
||||
const auto snd = LoadWAVSoundFile(GetResPath("audio/sine_48S16Stereo.wav").c_str());
|
||||
const auto src = PlayStereo(snd, {20.f, SR_Once, 0.5f});
|
||||
EXPECT_NE(snd, InvalidSourceRef);
|
||||
|
||||
auto src = PlayStereo(snd, {20.f, SR_Once, 0.5f});
|
||||
EXPECT_NE(snd, InvalidSoundRef);
|
||||
EXPECT_EQ(GetSourceState(src), SS_Playing);
|
||||
|
||||
while (GetSourceState(src) == SS_Playing)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
EXPECT_EQ(GetSourceState(src), SS_Stopped);
|
||||
|
||||
AudioShutdown();
|
||||
// replay it
|
||||
src = PlayStereo(snd, {20.f, SR_Once, 0.5f});
|
||||
EXPECT_NE(snd, InvalidSoundRef);
|
||||
EXPECT_EQ(GetSourceState(src), SS_Playing);
|
||||
|
||||
while (GetSourceState(src) == SS_Playing)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
EXPECT_EQ(GetSourceState(src), SS_Stopped);
|
||||
}
|
||||
|
||||
TEST(Audio, StreamWAV) {
|
||||
start_timer();
|
||||
|
||||
EXPECT_TRUE(AudioInit());
|
||||
TEST_F(Audio, StreamWAV) {
|
||||
EXPECT_TRUE(initialized);
|
||||
|
||||
const auto src = StreamWAVFileStereo(GetResPath("audio/sine_48S16Stereo.wav").c_str(), {20.f, SR_Once, 0.5f});
|
||||
EXPECT_NE(src, InvalidSourceRef);
|
||||
while (GetSourceState(src) == SS_Initial)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
EXPECT_EQ(GetSourceState(src), SS_Playing);
|
||||
while (GetSourceState(src) == SS_Playing)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
AudioShutdown();
|
||||
|
||||
stop_timer();
|
||||
EXPECT_EQ(GetSourceState(src), SS_Stopped);
|
||||
}
|
||||
|
||||
TEST_F(Audio, Timestamps) {
|
||||
EXPECT_TRUE(initialized);
|
||||
|
||||
const auto src = StreamWAVFileStereo(GetResPath("audio/sine_48S16Stereo.wav").c_str(), {20.f, SR_Once, 0.5f});
|
||||
EXPECT_NE(src, InvalidSourceRef);
|
||||
|
||||
time_ns duration = GetSourceDuration(src);
|
||||
EXPECT_EQ(duration, hg::time_from_sec(2)); // WARNING! Don't forget to change this test if you modify the input wav file.
|
||||
|
||||
hg::SourceState state;
|
||||
|
||||
while ((state = GetSourceState(src)) == SS_Initial) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
EXPECT_EQ(state, SS_Playing);
|
||||
|
||||
hg::time_ns constexpr t_break = hg::time_from_ms(1200);
|
||||
hg::time_ns constexpr t_rewind = hg::time_from_ms(200);
|
||||
hg::time_ns t_elapsed = 0;
|
||||
|
||||
while ((state == SS_Playing) && (t_elapsed < t_break)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
state = GetSourceState(src);
|
||||
t_elapsed = GetSourceTimecode(src);
|
||||
}
|
||||
|
||||
// Go back to t = 200ms.
|
||||
EXPECT_TRUE(SetSourceTimecode(src, t_rewind));
|
||||
|
||||
// Remember! This is asynchronous. It means that if we call GetSourceTimecode just after, it may not return 200ms.
|
||||
// This loop should run until the timestamp is set or the call was ignored and the stream ended. The latter being an error.
|
||||
int t_wait_ms = 50;
|
||||
while ((state == SS_Playing) && (t_elapsed > t_break)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(t_wait_ms));
|
||||
state = GetSourceState(src);
|
||||
t_elapsed = GetSourceTimecode(src);
|
||||
}
|
||||
|
||||
EXPECT_EQ(state, SS_Playing);
|
||||
EXPECT_LT(t_elapsed, t_break);
|
||||
// We must be closer to t_rewind than t_break.
|
||||
EXPECT_LT(abs(t_elapsed - t_rewind), abs(t_elapsed - t_break));
|
||||
|
||||
// Play the remaining of the audio stream.
|
||||
while (GetSourceState(src) == SS_Playing) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
EXPECT_EQ(GetSourceState(src), SS_Stopped);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,9 @@
|
|||
#include "engine/scene_forward_pipeline.h"
|
||||
#include "engine/scene_lua_vm.h"
|
||||
#include "engine/scene_systems.h"
|
||||
#if HG_ENABLE_BULLET3_SCENE_PHYSICS
|
||||
#include "engine/scene_bullet3_physics.h"
|
||||
#endif
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
|
@ -604,18 +607,20 @@ TEST(Scene, LuaScriptWriteToG) {
|
|||
}
|
||||
|
||||
//
|
||||
#if HG_ENABLE_SCENE_PHYSICS_API
|
||||
#if HG_ENABLE_BULLET3_SCENE_PHYSICS
|
||||
TEST(Scene, PhysicDynamicRigidBodyFreefall) {
|
||||
Scene scene;
|
||||
auto sphere = CreatePhysicSphere(scene, 0.5, Mat4::Identity, {}, {}, 1.f);
|
||||
|
||||
SceneNewtonPhysics physics;
|
||||
SceneSyncToSystemsFromAssets(scene, &physics);
|
||||
SceneBullet3Physics physics;
|
||||
SceneSyncToSystemsFromAssets(scene, physics);
|
||||
|
||||
SceneClocks clocks;
|
||||
for (int i = 0; i < 16; ++i)
|
||||
SceneUpdateSystems(scene, clocks, time_from_ms(16), &physics);
|
||||
|
||||
time_ns physics_step = time_from_ms(10);
|
||||
int max_physics_step = 64;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
SceneUpdateSystems(scene, clocks, time_from_ms(16), physics, physics_step, max_physics_step);
|
||||
}
|
||||
EXPECT_LT(GetT(sphere.GetTransform().GetWorld()).y, -0.3f);
|
||||
}
|
||||
|
||||
|
@ -624,13 +629,14 @@ TEST(Scene, PhysicKinematicRigidBodyNoFreefall) {
|
|||
auto sphere = CreatePhysicSphere(scene, 0.5, Mat4::Identity, {}, {}, 1.f);
|
||||
sphere.GetRigidBody().SetType(RBT_Kinematic);
|
||||
|
||||
SceneNewtonPhysics physics;
|
||||
SceneBullet3Physics physics;
|
||||
physics.SceneCreatePhysicsFromAssets(scene);
|
||||
|
||||
time_ns dt = time_from_ms(10);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
scene.ReadyWorldMatrices();
|
||||
physics.StepSimulation();
|
||||
physics.SyncDynamicBodiesToScene(scene);
|
||||
physics.StepSimulation(dt);
|
||||
physics.SyncBodiesFromScene(scene);
|
||||
}
|
||||
|
||||
EXPECT_EQ(GetT(sphere.GetTransform().GetWorld()).y, 0.f);
|
||||
|
@ -641,13 +647,14 @@ TEST(Scene, PhysicDynamicVsStaticRigidBodyCollisionCallback) {
|
|||
auto sphere = CreatePhysicSphere(scene, 0.5, TranslationMat4({0, 5, 0}), {}, {}, 1.f);
|
||||
auto ground = CreatePhysicCube(scene, {10, 1, 10}, TranslationMat4({0, -0.5f, 0}), {}, {}, 0.f);
|
||||
|
||||
SceneNewtonPhysics physics;
|
||||
SceneBullet3Physics physics;
|
||||
physics.SceneCreatePhysicsFromAssets(scene);
|
||||
physics.NodeStartTrackingCollisionEvents(sphere.ref);
|
||||
|
||||
time_ns dt = time_from_ms(10);
|
||||
size_t collision_count = 0;
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
physics.StepSimulation();
|
||||
physics.StepSimulation(dt);
|
||||
NodeNodeContacts node_node_contacts;
|
||||
physics.CollectCollisionEvents(scene, node_node_contacts);
|
||||
collision_count += node_node_contacts.size();
|
||||
|
@ -662,21 +669,20 @@ TEST(Scene, PhysicKinematicRigidBodyCollideWorld) {
|
|||
sphere.GetRigidBody().SetType(RBT_Kinematic);
|
||||
auto ground = CreatePhysicCube(scene, {10, 1, 10}, TranslationMat4({0, -0.5f, 0}), {}, {}, 0.f);
|
||||
|
||||
SceneNewtonPhysics physics;
|
||||
SceneBullet3Physics physics;
|
||||
physics.SceneCreatePhysicsFromAssets(scene);
|
||||
|
||||
const auto collisions = physics.NodeCollideWorld(sphere.ref, sphere.GetTransform().GetWorld());
|
||||
const auto collisions = physics.NodeCollideWorld(sphere, sphere.GetTransform().GetWorld());
|
||||
EXPECT_GT(collisions.size(), 0);
|
||||
}
|
||||
|
||||
//
|
||||
TEST(Scene, PhysicRaycastFirstHit) {
|
||||
Scene scene;
|
||||
CreatePhysicCube(scene, {1, 1, 1}, Mat4::Identity, InvalidModelRef, {}, 0.f);
|
||||
const auto closest_node = CreatePhysicCube(scene, {.1f, .1f, .1f}, TranslationMat4({0, 0, -1.f}), InvalidModelRef, {}, 0.f);
|
||||
CreatePhysicCube(scene, {.1f, .1f, .1f}, TranslationMat4({0, 0, 1.f}), InvalidModelRef, {}, 0.f);
|
||||
|
||||
SceneNewtonPhysics physics;
|
||||
SceneBullet3Physics physics;
|
||||
physics.SceneCreatePhysicsFromAssets(scene);
|
||||
|
||||
const auto out = physics.RaycastFirstHit(scene, {0, 0, -10.f}, {0, 0, 10.f});
|
||||
|
@ -690,7 +696,7 @@ TEST(Scene, PhysicRaycastFirstHitOutOfReach) {
|
|||
const auto closest_node = CreatePhysicCube(scene, {.1f, .1f, .1f}, TranslationMat4({0, 0, -1.f}), InvalidModelRef, {}, 0.f);
|
||||
CreatePhysicCube(scene, {.1f, .1f, .1f}, TranslationMat4({0, 0, 1.f}), InvalidModelRef, {}, 0.f);
|
||||
|
||||
SceneNewtonPhysics physics;
|
||||
SceneBullet3Physics physics;
|
||||
physics.SceneCreatePhysicsFromAssets(scene);
|
||||
|
||||
const auto out = physics.RaycastFirstHit(scene, {0, 0, -10.f}, {0, 0, -2.f});
|
||||
|
@ -703,14 +709,24 @@ TEST(Scene, PhysicRaycastAllHits) {
|
|||
const auto closest_node = CreatePhysicCube(scene, {.1f, .1f, .1f}, TranslationMat4({0, 0, -1.f}), InvalidModelRef, {}, 0.f);
|
||||
const auto farthest_node = CreatePhysicCube(scene, {.1f, .1f, .1f}, TranslationMat4({0, 0, 1.f}), InvalidModelRef, {}, 0.f);
|
||||
|
||||
SceneNewtonPhysics physics;
|
||||
SceneBullet3Physics physics;
|
||||
physics.SceneCreatePhysicsFromAssets(scene);
|
||||
|
||||
const auto out = physics.RaycastAllHits(scene, {0, 0, -10.f}, {0, 0, 10.f});
|
||||
EXPECT_EQ(out.size(), 3);
|
||||
EXPECT_EQ(out[0].node, closest_node);
|
||||
EXPECT_EQ(out[1].node, middle_node);
|
||||
EXPECT_EQ(out[2].node, farthest_node);
|
||||
|
||||
// Nodes are not returned in distance order.
|
||||
// Here we just check if there's no duplicates/weird result (it should not).
|
||||
int found = 0;
|
||||
const hg::Node nodes[] = {middle_node, closest_node, farthest_node};
|
||||
for (int j = 0; j < 3; j++) {
|
||||
for (int i = 0; i < out.size(); i++) {
|
||||
if (out[i].node == nodes[j]) {
|
||||
found++;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(found, j+1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Scene, PhysicRaycastAllHitsOutOfReach) {
|
||||
|
@ -719,13 +735,13 @@ TEST(Scene, PhysicRaycastAllHitsOutOfReach) {
|
|||
CreatePhysicCube(scene, {.1f, .1f, .1f}, TranslationMat4({0, 0, -1.f}), InvalidModelRef, {}, 0.f);
|
||||
CreatePhysicCube(scene, {.1f, .1f, .1f}, TranslationMat4({0, 0, 1.f}), InvalidModelRef, {}, 0.f);
|
||||
|
||||
SceneNewtonPhysics physics;
|
||||
SceneBullet3Physics physics;
|
||||
physics.SceneCreatePhysicsFromAssets(scene);
|
||||
|
||||
const auto out = physics.RaycastAllHits(scene, {0, 0, -10.f}, {0, 0, -2.f});
|
||||
EXPECT_TRUE(out.empty());
|
||||
}
|
||||
#endif
|
||||
#endif // HG_ENABLE_BULLET3_SCENE_PHYSICS
|
||||
|
||||
//
|
||||
TEST(Scene, ComponentGarbageCollection) {
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.1.0
|
||||
3.1.1
|
||||
|
|
28
readme.md
28
readme.md
|
@ -55,13 +55,13 @@ Audio API
|
|||
<a name="subsection_1b"></a>
|
||||
## Screenshots
|
||||
|
||||
The following screenshots were captured on a 1080GTX in 1080P running at 60FPS.
|
||||
The following screenshots were captured on a 2070RTX in 1080P running at 60FPS, GI is performed using screen space raytracing and does not require RTX capable hardware.
|
||||
|
||||
![alt text](https://raw.githubusercontent.com/harfang3d/image-storage/main/portfolio/2.0.111/sun_temple_aaa.png)
|
||||
![alt text](https://raw.githubusercontent.com/harfang3d/image-storage/main/portfolio/3.1.1/cyber_city_aaa.png)
|
||||
|
||||
![alt text](https://raw.githubusercontent.com/harfang3d/image-storage/main/portfolio/2.0.111/sun_temple_aaa_2.png)
|
||||
![alt text](https://raw.githubusercontent.com/harfang3d/image-storage/main/portfolio/3.1.1/cyber_city_aaa_2.png)
|
||||
|
||||
*(Sun Temple, courtesy of the Open Research Content Archive (ORCA))*
|
||||
*(CyberPunk City, CyberPunk Girl and Robot R32 by art-equilibrium, ILranch and ZeroArt3d)*
|
||||
|
||||
<a name="section_2"></a>
|
||||
# Download
|
||||
|
@ -229,3 +229,23 @@ Given a version number MAJOR.MINOR.PATCH, increment the:
|
|||
Harfang is licensed under the GPLv3, LGPLv3 and a commercial license:<br>
|
||||
https://www.harfang3d.com/licenses/
|
||||
|
||||
# More screenshots...
|
||||
|
||||
![alt text](https://raw.githubusercontent.com/harfang3d/image-storage/main/portfolio/3.1.1/sun_temple_aaa.png)
|
||||
|
||||
![alt text](https://raw.githubusercontent.com/harfang3d/image-storage/main/portfolio/3.1.1/sun_temple_aaa_2.png)
|
||||
|
||||
*(Sun Temple, courtesy of the Open Research Content Archive (ORCA))*
|
||||
|
||||
![alt text](https://raw.githubusercontent.com/harfang3d/image-storage/main/portfolio/3.1.1/cafe_exterior_aaa.png)
|
||||
|
||||
![alt text](https://raw.githubusercontent.com/harfang3d/image-storage/main/portfolio/3.1.1/cafe_exterior_aaa_2.png)
|
||||
|
||||
*(Bistro, courtesy of the Open Research Content Archive (ORCA))*
|
||||
|
||||
![alt text](https://raw.githubusercontent.com/harfang3d/image-storage/main/portfolio/3.1.1/sponza_atrium_aaa.png)
|
||||
|
||||
![alt text](https://raw.githubusercontent.com/harfang3d/image-storage/main/portfolio/3.1.1/sponza_atrium_aaa_2.png)
|
||||
|
||||
*(Sponza Atrium GLTF, courtesy of Crytek/Themaister)*
|
||||
|
||||
|
|
|
@ -1,3 +1,22 @@
|
|||
# [3.1.1] - 2021-12-31
|
||||
|
||||
This minor release brings several fixes, mainly in the Bullet Physics API.
|
||||
|
||||
### Engine
|
||||
|
||||
- Added a missing texture render target flag (`TF_RenderTarget`).
|
||||
- Added `SetSourceStreamTimecode()` to the audio API.
|
||||
|
||||
### Physics
|
||||
|
||||
- Updated the Physics unit tests.
|
||||
- Fixed an issue in the `Kinematic` physics matrix.
|
||||
- `SetRigidBodyAngularDamping()` now takes a `float` instead of a `Vec3`.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fixed several typos in the documentation.
|
||||
|
||||
# [3.1.0] - 2021-12-13
|
||||
|
||||
This minor release brings several improvements and fixes, mainly in the Bullet Physics API.
|
||||
|
|
Loading…
Reference in New Issue
Block a user