diff --git a/docs/reference/changelog-r2024.md b/docs/reference/changelog-r2024.md index 18b6cc96618..c5698d71eb1 100644 --- a/docs/reference/changelog-r2024.md +++ b/docs/reference/changelog-r2024.md @@ -5,6 +5,7 @@ Released on December **th, 2023. - New Features - **Change the name of the web scene format from `X3D` to `W3D` ([#6280](https://github.com/cyberbotics/webots/pull/6280)).** - Removed support for macOS 11 "Big Sur" and added support for macOS 14 "Sonoma" ([#6580](https://github.com/cyberbotics/webots/pull/6580)). + - Added the `indirectFieldAccess` tag to allow the `fields` variable to be used in proto templates without referencing a specific field ([#6614](https://github.com/cyberbotics/webots/pull/6614)). - Added a method to include all subtypes of a node type in a PROTO field restriction ([#6574](https://github.com/cyberbotics/webots/pull/6574)). - Enhancements - Improved the image range of the rotating [Lidar](lidar.md) ([#6324](https://github.com/cyberbotics/webots/pull/6324)). @@ -18,5 +19,8 @@ Released on December **th, 2023. - Fixed crashes (with some graphics cards) caused by references to unused GLSL uniforms ([#6587](https://github.com/cyberbotics/webots/pull/6587)). - Fixed `Brake`s added to the second axis of a `Hinge2Joint` being applied to the non-transformed axis ([#6584](https://github.com/cyberbotics/webots/pull/6584)). - Fixed invalid absolute sound file path resulted in crash ([#6593](https://github.com/cyberbotics/webots/pull/6593)) + - Fixed Speaker relative sound file path not working with external controller ([#6605](https://github.com/cyberbotics/webots/pull/6605)). - Fixed the bug that when the language is Python, getTargets() cannot correctly obtain the multi-target data detected by the radar ([#6606](https://github.com/cyberbotics/webots/pull/6606)) + - Fixed incomplete loading while minimized under some windowing systems ([#6617](https://github.com/cyberbotics/webots/pull/6617)). + - Fixed unitialized sliding friction when using asymmetric rolling friction ([#6618](https://github.com/cyberbotics/webots/pull/6618)). - Fixed handling of remote assets from unofficial sources ([#6585](https://github.com/cyberbotics/webots/pull/6585)). diff --git a/docs/reference/javascript-procedural-proto.md b/docs/reference/javascript-procedural-proto.md index d10426701cd..9bf26da048b 100644 --- a/docs/reference/javascript-procedural-proto.md +++ b/docs/reference/javascript-procedural-proto.md @@ -27,6 +27,8 @@ The first represents the effective value of the field (for instance the one defi - As shown in [this table](#vrml97-type-to-javascript-type-conversion), the conversion of a VRML97 node is an object. This object contains the following keys: "node\_name" containing the VRML97 node name and "fields" which is in turn an object containing the JavaScript representation of the VRML97 node fields. This object is equal to `undefined` if the VRML97 node is not defined (`NULL`). +- By default, the parser only detects fields that are accessed directly in the template statements (i.e. `fields.appearance`). +If you would like to access the `fields` object without referencing a specific field, you can add the following line at the beginning of the PROTO file: `# tags: indirectFieldAccess`. - Objects that are part of [ECMA-262](http://www.ecma-international.org/publications/standards/Ecma-262.htm) are built-in and globally accessible, such as `Math`, `Date` and `String`. - The `context` object provides contextual information about the PROTO. Table [this table](#content-of-the-context-object) shows the available information and the corresponding keys. @@ -823,6 +825,14 @@ The location of this path can be retrieved from the `context` field object, see %end +### PROTO Regeneration +When a field used in a template statement is modified, the PROTO node is regenerated. +This means that the template statements are re-evaluated and the PROTO node is reloaded in the world. +For most nodes, this behavior should not affect the simulation. +However, special care should be taken when using PROTOs that have side effects (e.g. writing to a file). +Additionally, Robot nodes will restart their controllers when regenerated. +Using `tags: indirectFieldAccess` in the PROTO file will cause the PROTO to be regenerated whenever any field is modified, even if it is not used in a template statement. + ### Optimization By default, PROTO files are considered to be deterministic. diff --git a/docs/reference/lua-procedural-proto.md b/docs/reference/lua-procedural-proto.md index 04cbf5ed519..2f1f9aea87a 100644 --- a/docs/reference/lua-procedural-proto.md +++ b/docs/reference/lua-procedural-proto.md @@ -24,6 +24,8 @@ The conversion between the VRML97 types and the Lua types is detailed in [this t This dictionary contains the following keys: "node\_name" containing the VRML97 node name and "fields" which is a dictionary containing the Lua representation of the VRML97 node fields. This dictionary is equal to `nil` if the VRML97 node is not defined (`NULL`). For example, in the SimpleStairs example below, the `fields.appearance.node_name` key contains the `'Appearance'` string. +- By default, the parser only detects fields that are accessed directly in the template statements (i.e. `fields.appearance`). +If you would like to access the `fields` object without referencing a specific field, you can add the following line at the beginning of the PROTO file: `# tags: indirectFieldAccess`. - The `context` dictionary provides contextual information about the PROTO. Table [this table](#content-of-the-context-dictionary) shows the available information and its corresponding keys. - The VRML97 comment ("#") prevails over the Lua statements. @@ -88,6 +90,14 @@ The following standard fonts are available to write on the texture: In addition to these fonts, it is possible to add other TrueType fonts file in your `PROJECT_HOME/fonts` directory. +### PROTO Regeneration +When a field used in a template statement is modified, the PROTO node is regenerated. +This means that the template statements are re-evaluated and the PROTO node is reloaded in the world. +For most nodes, this behavior should not affect the simulation. +However, special care should be taken when using PROTOs that have side effects (e.g. writing to a file). +Additionally, Robot nodes will restart their controllers when regenerated. +Using `tags: indirectFieldAccess` in the PROTO file will cause the PROTO to be regenerated whenever any field is modified, even if it is not used in a template statement. + ### Optimization By default, PROTO files are considered to be deterministic. diff --git a/docs/reference/speaker.md b/docs/reference/speaker.md index 10806318332..06cff9f7bff 100644 --- a/docs/reference/speaker.md +++ b/docs/reference/speaker.md @@ -111,9 +111,11 @@ Finally, the boolean `loop` argument defines if the sound will be played only on It is possible to change the volume, pitch, balance, and loop parameters of a sound currently playing by calling again the `wb_speaker_play_sound` function with the same speakers and `sound` arguments. -> **Note**: The path to the sound file should be defined either absolutely or relatively. -If defined relatively, it will be searched first relatively to the robot controller folder. -If not found there and if the robot is a PROTO, it will be searched relatively to the PROTO folder of the robot. +> **Note**: The path to the sound file can be defined either absolutely or relatively. +If the file path is accessible by the robot controller either absolutely or relative to its working directory, it is loaded by the controller and its contents are streamed to Webots. +If the file is not found by the controller, the given path is passed to Webots. +Webots will first search for the path relative to the robot controller folder. +If the file is not found relative to the controller folder and the robot is a PROTO, Webots will search for the sound file relative to the folder of the PROTO file. --- diff --git a/include/wren/scene.h b/include/wren/scene.h index 9788bbb2935..ea0d7fc1d0d 100644 --- a/include/wren/scene.h +++ b/include/wren/scene.h @@ -42,8 +42,9 @@ void wr_scene_apply_pending_updates(WrScene *scene); /* The 'materialName' parameter is optional (can be set to NULL for default), and if set, force the renderables to use the named material */ -void wr_scene_render(WrScene *scene, const char *material_name, bool culling); -void wr_scene_render_to_viewports(WrScene *scene, int count, WrViewport **viewports, const char *material_name, bool culling); +void wr_scene_render(WrScene *scene, const char *material_name, bool culling, bool offScreen); +void wr_scene_render_to_viewports(WrScene *scene, int count, WrViewport **viewports, const char *material_name, bool culling, + bool offScreen); void wr_scene_set_ambient_light(const float *ambient_light); diff --git a/src/controller/c/speaker.c b/src/controller/c/speaker.c index 306d63bbb25..fdf8472faf9 100644 --- a/src/controller/c/speaker.c +++ b/src/controller/c/speaker.c @@ -40,6 +40,8 @@ struct Sound { bool need_update; bool need_stop; bool is_playing; + unsigned char *upload_data; // sound data for uploading to webots + int upload_size; // size of sound data struct Sound *next; }; @@ -257,6 +259,18 @@ static void speaker_write_request(WbDevice *d, WbRequest *r) { else request_write_char(r, 0); } + + request_write_int32(r, sound->upload_size); + if (sound->upload_size) { + /* Sound data available to stream, send it one time and discard it. + * Webots will cache the data inside the device. + */ + request_write_data(r, sound->upload_data, sound->upload_size); + free(sound->upload_data); + sound->upload_data = NULL; + sound->upload_size = 0; + } + sound->need_update = false; sound = sound->next; } @@ -298,6 +312,43 @@ static void speaker_toggle_remote(WbDevice *d, WbRequest *r) { // nothing to do. } +/* Try to read sound file contents if it is found by given path. */ +static void speaker_try_load_sound_local(Sound *sound, const char *sound_file_name) { + FILE *fp; + + sound->upload_data = NULL; + sound->upload_size = 0; + + if ((fp = fopen(sound_file_name, "rb")) != NULL) { + if (fseek(fp, 0L, SEEK_END) == 0) { + const long file_size = ftell(fp); + if (file_size != -1) { + if (fseek(fp, 0L, SEEK_SET) == 0) { + size_t consumed = 0U; + sound->upload_data = malloc(file_size); + sound->upload_size = (int)file_size; + while (consumed < file_size) { + const size_t read = fread(&sound->upload_data[consumed], sizeof(unsigned char), file_size - consumed, fp); + if (ferror(fp)) { + free(sound->upload_data); + sound->upload_data = NULL; + sound->upload_size = 0; + break; + } + consumed += read; + } + } + } + } + + if (sound->upload_data == NULL) { + fprintf(stderr, "Warning: %s() failed to read sound file '%s' :", __FUNCTION__, sound_file_name); + perror(NULL); + } + fclose(fp); + } +} + static void speaker_play_sound(WbDevice *device, const char *sound_name, double volume, double pitch, double balance, bool loop, int side) { Sound *sound = speaker_get_sound_if_exist(device, sound_name); @@ -309,6 +360,8 @@ static void speaker_play_sound(WbDevice *device, const char *sound_name, double memcpy(sound->sound_file, sound_name, l); sound->next = ((Speaker *)device->pdata)->sound_list; ((Speaker *)device->pdata)->sound_list = sound; + + speaker_try_load_sound_local(sound, sound_name); } sound->volume = volume; sound->pitch = pitch; diff --git a/src/webots/engine/WbSimulationCluster.cpp b/src/webots/engine/WbSimulationCluster.cpp index eec18e00ef9..2d9abbeeb66 100644 --- a/src/webots/engine/WbSimulationCluster.cpp +++ b/src/webots/engine/WbSimulationCluster.cpp @@ -320,8 +320,15 @@ void WbSimulationCluster::fillSurfaceParameters(const WbContactProperties *cp, c // handle rolling friction if (rho[0] > 0 || rho[1] > 0 || rho[2] > 0) { contact->surface.mode = contact->surface.mode | dContactRolling; - if (rho[1] > 0 || rho[2] > 0) + if (rho[1] > 0 || rho[2] > 0) { + // ODE will use asymmetric sliding friction if we set dContactAxisDep (because dContactAxisDep + // == dContactMu2), so we need to make sure mu2 is set. If it wasn't already set above, we + // want symmetric sliding friction which means setting mu2 = mu (like ODE would do if + // dContactMu2 was not set.) + if (!(contact->surface.mode & dContactMu2)) + contact->surface.mu2 = contact->surface.mu; contact->surface.mode = contact->surface.mode | dContactAxisDep; + } contact->surface.rho = rho[0]; contact->surface.rho2 = rho[1]; diff --git a/src/webots/gui/WbView3D.cpp b/src/webots/gui/WbView3D.cpp index f7ce3bb2849..b60cc9bd591 100644 --- a/src/webots/gui/WbView3D.cpp +++ b/src/webots/gui/WbView3D.cpp @@ -1111,8 +1111,9 @@ void WbView3D::setWorld(WbSimulationWorld *w) { WbWrenOpenGlContext::doneWren(); - // first rendering without culling to make sure every meshes/textures are actually loaded on the GPU - renderNow(false); + // first rendering is offscreen without culling to make sure every meshes/textures are actually + // loaded on the GPU + renderNow(false, true); } void WbView3D::restoreOptionalRendering(const QStringList &enabledCenterOfMassNodeNames, @@ -1441,7 +1442,7 @@ void WbView3D::resizeWren(int width, int height) { emit resized(); } -void WbView3D::renderNow(bool culling) { +void WbView3D::renderNow(bool culling, bool offScreen) { if (!wr_gl_state_is_initialized()) initialize(); @@ -1461,7 +1462,7 @@ void WbView3D::renderNow(bool culling) { WbWrenOpenGlContext::doneWren(); } else #endif - WbWrenWindow::renderNow(culling); + WbWrenWindow::renderNow(culling, offScreen); mLastRefreshTimer.start(); emit mainRenderingEnded(mPhysicsRefresh); diff --git a/src/webots/gui/WbView3D.hpp b/src/webots/gui/WbView3D.hpp index d81d292a6c8..be3dec0bf64 100644 --- a/src/webots/gui/WbView3D.hpp +++ b/src/webots/gui/WbView3D.hpp @@ -97,7 +97,7 @@ public slots: protected slots: // cppcheck-suppress virtualCallInConstructor - void renderNow(bool culling = true) override; + void renderNow(bool culling = true, bool offScreen = false) override; protected: void initialize() override; diff --git a/src/webots/gui/WbWrenWindow.cpp b/src/webots/gui/WbWrenWindow.cpp index 0d404cd7131..44960f92495 100644 --- a/src/webots/gui/WbWrenWindow.cpp +++ b/src/webots/gui/WbWrenWindow.cpp @@ -200,8 +200,8 @@ void WbWrenWindow::renderLater() { } } -void WbWrenWindow::renderNow(bool culling) { - if (!isExposed() || !wr_gl_state_is_initialized()) +void WbWrenWindow::renderNow(bool culling, bool offScreen) { + if ((!isExposed() && !offScreen) || !wr_gl_state_is_initialized()) return; static int first = true; @@ -225,9 +225,10 @@ void WbWrenWindow::renderNow(bool culling) { WbWrenOpenGlContext::makeWrenCurrent(); - wr_scene_render(wr_scene_get_instance(), NULL, culling); + wr_scene_render(wr_scene_get_instance(), NULL, culling, offScreen); - WbWrenOpenGlContext::instance()->swapBuffers(this); + if (!offScreen) + WbWrenOpenGlContext::instance()->swapBuffers(this); WbWrenOpenGlContext::doneWren(); if (mVideoStreamingServer && mVideoStreamingServer->isNewFrameNeeded() && !first) diff --git a/src/webots/gui/WbWrenWindow.hpp b/src/webots/gui/WbWrenWindow.hpp index 9a49b17efe4..dee3c098876 100644 --- a/src/webots/gui/WbWrenWindow.hpp +++ b/src/webots/gui/WbWrenWindow.hpp @@ -65,7 +65,7 @@ public slots: protected: virtual void initialize(); - virtual void renderNow(bool culling = true); + virtual void renderNow(bool culling = true, bool offScreen = false); virtual void resizeWren(int width, int height); bool event(QEvent *event) override; diff --git a/src/webots/nodes/WbSpeaker.cpp b/src/webots/nodes/WbSpeaker.cpp index 425dfdcfe8a..3f90886949e 100644 --- a/src/webots/nodes/WbSpeaker.cpp +++ b/src/webots/nodes/WbSpeaker.cpp @@ -27,6 +27,7 @@ #include "../../controller/c/messages.h" +#include #include #include @@ -61,6 +62,10 @@ WbSpeaker::~WbSpeaker() { } mSoundSourcesMap.clear(); mPlayingSoundSourcesMap.clear(); + + foreach (QByteArray *buffer, mStreamedSoundDataMap) + delete buffer; + mStreamedSoundDataMap.clear(); } void WbSpeaker::postFinalize() { @@ -79,21 +84,8 @@ void WbSpeaker::handleMessage(QDataStream &stream) { int numberOfSound = 0; stream >> numberOfSound; for (int i = 0; i < numberOfSound; ++i) { - short size; - short side; - double volume; - double pitch; - double balance; - unsigned char loop; - stream >> size; - char soundFile[size]; - stream.readRawData(soundFile, size); - stream >> volume; - stream >> pitch; - stream >> balance; - stream >> side; - stream >> loop; - playSound(soundFile, volume, pitch, balance, (bool)loop, (int)side); + SoundPlayData playData(stream); + playSound(playData); } return; } @@ -194,18 +186,28 @@ void WbSpeaker::playText(const char *text, double volume) { } } -void WbSpeaker::playSound(const char *file, double volume, double pitch, double balance, bool loop, int side) { - QString filename = QString(file); - QString key = filename; - if (side == -1) +void WbSpeaker::playSound(SoundPlayData &playData) { + const QString filename(playData.file()); + QString key(filename); + if (playData.side() == -1) key += "_left"; - else if (side == 1) + else if (playData.side() == 1) key += "_right"; + // Controller streamed sound data available ? + if (playData.rawLength() && !mStreamedSoundDataMap.contains(filename)) + mStreamedSoundDataMap[filename] = new QByteArray(playData.rawData()); + QByteArray *cachedSoundData = NULL; + if (mStreamedSoundDataMap.contains(key)) + cachedSoundData = mStreamedSoundDataMap[key]; + if (!mSoundSourcesMap.contains(key)) { // this sound was never played QString path; + // check if sound data was streamed from controller + if (cachedSoundData) + path = filename; // check if the path is absolute - if (QDir::isAbsolutePath(filename) && QFile::exists(filename)) + if (path.isEmpty() && QDir::isAbsolutePath(filename) && QFile::exists(filename)) path = filename; // check if the path is relative to the controller if (path.isEmpty() && QFile::exists(mControllerDir + filename)) @@ -221,7 +223,18 @@ void WbSpeaker::playSound(const char *file, double volume, double pitch, double WbSoundSource *source = WbSoundEngine::createSource(); updateSoundSource(source); const QString extension = filename.mid(filename.lastIndexOf('.') + 1).toLower(); - const WbSoundClip *soundClip = WbSoundEngine::sound(path, extension, NULL, balance, side); + + // Use controller streamed sound data if available. + QBuffer *device = NULL; + if (cachedSoundData) { + device = new QBuffer(cachedSoundData); + device->open(QBuffer::ReadOnly); + } + + const WbSoundClip *soundClip = WbSoundEngine::sound(path, extension, device, playData.balance(), playData.side()); + delete device; + device = NULL; + if (!soundClip) { this->warn(tr("Impossible to play '%1'. Make sure the file format is supported (8 or 16 bits, mono or stereo wave).\n") .arg(filename)); @@ -237,9 +250,9 @@ void WbSpeaker::playSound(const char *file, double volume, double pitch, double mPlayingSoundSourcesMap[key] = source; if (!source->isPlaying()) // this sound was already played but is over source->play(); - source->setLooping(loop); - source->setGain(volume); - source->setPitch(pitch); + source->setLooping(playData.loop()); + source->setGain(playData.volume()); + source->setPitch(playData.pitch()); if (WbSimulationState::instance()->isPaused() || WbSimulationState::instance()->isStep()) source->pause(); } @@ -280,3 +293,24 @@ void WbSpeaker::updateSoundSource(WbSoundSource *source) { source->setVelocity(linearVelocity()); source->setDirection(rotationMatrix() * WbVector3(0, 1, 0)); } + +WbSpeaker::SoundPlayData::SoundPlayData(QDataStream &stream) : mFile(), mRawData() { + short size; + unsigned char loopByte; + stream >> size; + char soundFile[size]; + stream.readRawData(soundFile, size); + mFile = QString(soundFile); + + stream >> mVolume; + stream >> mPitch; + stream >> mBalance; + stream >> mSide; + stream >> loopByte; + mLoop = (bool)loopByte; + stream >> mRawLength; + if (mRawLength) { + mRawData.resize(mRawLength); + stream.readRawData(mRawData.data(), mRawLength); + } +} diff --git a/src/webots/nodes/WbSpeaker.hpp b/src/webots/nodes/WbSpeaker.hpp index af76aa1695f..cb2076f1303 100644 --- a/src/webots/nodes/WbSpeaker.hpp +++ b/src/webots/nodes/WbSpeaker.hpp @@ -20,6 +20,7 @@ #include class WbSoundSource; +class QDataStream; class WbSpeaker : public WbSolidDevice { Q_OBJECT @@ -39,12 +40,39 @@ class WbSpeaker : public WbSolidDevice { void postPhysicsStep() override; private: + // private data types + + // Data from wb_speaker_play_sound() API + class SoundPlayData { + public: + explicit SoundPlayData(QDataStream &); + + const QString &file() const { return mFile; } + double volume() const { return mVolume; } + double pitch() const { return mPitch; } + double balance() const { return mBalance; } + bool loop() const { return mLoop; } + short side() const { return mSide; } + int rawLength() const { return mRawLength; } + QByteArray &rawData() { return mRawData; } + + private: + QString mFile; + double mVolume; + double mPitch; + double mBalance; + bool mLoop; + short mSide; + int mRawLength; + QByteArray mRawData; + }; + // private functions WbSpeaker &operator=(const WbSpeaker &); // non copyable WbNode *clone() const override { return new WbSpeaker(*this); } void init(); - void playSound(const char *file, double volume, double pitch, double balance, bool loop, int side); + void playSound(SoundPlayData &playData); void playText(const char *text, double volume); void stop(const char *sound); void stopAll(); @@ -53,6 +81,7 @@ class WbSpeaker : public WbSolidDevice { QMap mSoundSourcesMap; QMap mPlayingSoundSourcesMap; + QMap mStreamedSoundDataMap; QString mControllerDir; QString mEngine; diff --git a/src/webots/nodes/utils/WbVirtualRealityHeadset.cpp b/src/webots/nodes/utils/WbVirtualRealityHeadset.cpp index 07f7653fce8..9beeeb7bbad 100644 --- a/src/webots/nodes/utils/WbVirtualRealityHeadset.cpp +++ b/src/webots/nodes/utils/WbVirtualRealityHeadset.cpp @@ -406,7 +406,7 @@ void WbVirtualRealityHeadset::updateOrientationAndPosition() { if (mWrenViewports[0] && mSystem) { WbWrenOpenGlContext::makeWrenCurrent(); - wr_scene_render_to_viewports(wr_scene_get_instance(), 2, mWrenViewports, NULL, true); + wr_scene_render_to_viewports(wr_scene_get_instance(), 2, mWrenViewports, NULL, true, false); WbWrenOpenGlContext::doneWren(); vr::VRCompositor()->Submit(vr::Eye_Left, mTextureReferences[LEFT], mTextureBounds); vr::VRCompositor()->Submit(vr::Eye_Right, mTextureReferences[RIGHT], mTextureBounds); diff --git a/src/webots/sound/WbSoundClip.cpp b/src/webots/sound/WbSoundClip.cpp index 2f6f9e95770..867bd9b94d3 100644 --- a/src/webots/sound/WbSoundClip.cpp +++ b/src/webots/sound/WbSoundClip.cpp @@ -28,7 +28,7 @@ #include #endif -WbSoundClip::WbSoundClip() : mDevice(NULL), mBuffer(0), mSide(0), mBalance(0.0) { +WbSoundClip::WbSoundClip() : mBuffer(0), mSide(0), mBalance(0.0) { } WbSoundClip::~WbSoundClip() { @@ -46,7 +46,6 @@ void WbSoundClip::load(const QString &filename, const QString &extension, QIODev mFilename = wave.filename(); mSide = side; mBalance = balance; - mDevice = device; load(&wave); } diff --git a/src/webots/sound/WbSoundClip.hpp b/src/webots/sound/WbSoundClip.hpp index fca133fea05..0869a24066c 100644 --- a/src/webots/sound/WbSoundClip.hpp +++ b/src/webots/sound/WbSoundClip.hpp @@ -38,7 +38,6 @@ class WbSoundClip { protected: QString mFilename; - QIODevice *mDevice; unsigned int mBuffer; int mSide; // 0: both sides, -1: left only, 1: right only double mBalance; diff --git a/src/webots/vrml/WbProtoModel.cpp b/src/webots/vrml/WbProtoModel.cpp index dbcae12e951..55b0347c00f 100644 --- a/src/webots/vrml/WbProtoModel.cpp +++ b/src/webots/vrml/WbProtoModel.cpp @@ -69,6 +69,7 @@ WbProtoModel::WbProtoModel(WbTokenizer *tokenizer, const QString &worldPath, con mDocumentationUrl = tokenizer->documentationUrl(); mTemplateLanguage = tokenizer->templateLanguage(); mIsDeterministic = !mTags.contains("nonDeterministic"); + mHasIndirectFieldAccess = mTags.contains("indirectFieldAccess"); WbParser parser(tokenizer); while (tokenizer->peekWord() == "EXTERNPROTO" || tokenizer->peekWord() == "IMPORTABLE") // consume EXTERNPROTO declarations @@ -209,15 +210,23 @@ WbProtoModel::WbProtoModel(WbTokenizer *tokenizer, const QString &worldPath, con previousToken = token; token = tokenizer->nextToken(); + if (mHasIndirectFieldAccess) { + foreach (WbFieldModel *model, mFieldModels) + model->setTemplateRegenerator(true); + } + if (token->isTemplateStatement()) { mTemplate = true; - foreach (WbFieldModel *model, mFieldModels) { - // condition explanation: if (token contains modelName and not a Lua identifier containing modelName such as - // "my_awesome_modelName") - if (token->word().contains(QRegularExpression( - QString("(^|[^a-zA-Z0-9_])fields\\.%1($|[^a-zA-Z0-9_])").arg(QRegularExpression::escape(model->name()))))) { - model->setTemplateRegenerator(true); + if (!mHasIndirectFieldAccess) { // If the proto has indirect field access, we've already set the fields as template + // regenerators + foreach (WbFieldModel *model, mFieldModels) { + // condition explanation: if (token contains modelName and not a Lua identifier containing modelName such as + // "my_awesome_modelName") or (token contains fields and not a Lua identifier containing fields such as "my_fields") + if (token->word().contains( + QRegularExpression(QString("(^|\\W)fields\\.%1($|\\W)").arg(QRegularExpression::escape(model->name()))))) { + model->setTemplateRegenerator(true); + } } } } else if (readBaseType) { @@ -274,26 +283,29 @@ WbProtoModel::WbProtoModel(WbTokenizer *tokenizer, const QString &worldPath, con throw 0; } } else if (token->isString()) { - // check which parameter need to regenerate the template instance from inside a string - foreach (WbFieldModel *model, mFieldModels) { - // regex test cases: - // "You know nothing, John Snow." => false - // "%{=fields.model->name()}%" => false - // "%{= fields.model->name().value.x }% %{= fields.model->name().value.y }%" => true - // "abc %{= fields.model->name().value.y }% def" => true - // "%{= 17 % fields.model->name().value.y * 88 }%" => true - // "fields.model->name().value.y" => false - // "%{}% fields.model->name().value.y %{}%" => false - // "%{ a = \"fields.model->name().value.y\" }%" => false - // "%{= \"fields.model->name().value.y\" }%" => false - // "%{= fields.model->name().value.y }%" => true - if (token->word().contains(QRegularExpression(QString("%1(?:(?!%2|\").)*fields\\.%3(?:(?!%4|\").)*%5") - .arg(open) - .arg(close) - .arg(QRegularExpression::escape(model->name())) - .arg(close) - .arg(close)))) - model->setTemplateRegenerator(true); + if (!mHasIndirectFieldAccess) { // If the proto has indirect field access, we've already set the fields as template + // regenerators + // check which parameter need to regenerate the template instance from inside a string + foreach (WbFieldModel *model, mFieldModels) { + // regex test cases: + // "You know nothing, John Snow." => false + // "%{=fields.model->name()}%" => true + // "%{= fields.model->name().value.x }% %{= fields.model->name().value.y }%" => true + // "abc %{= fields.model->name().value.y }% def" => true + // "%{= 17 % fields.model->name().value.y * 88 }%" => true + // "fields.model->name().value.y" => false + // "%{}% fields.model->name().value.y %{}%" => false + // "%{ a = \"fields.model->name().value.y\" }%" => false + // "%{= \"fields.model->name().value.y\" }%" => false + // "%{= fields.model->name().value.y }%" => true + if (token->word().contains(QRegularExpression(QString("%1(?:(?!%2|\").)*fields\\.%3(?:(?!%4|\").)*%5") + .arg(open) + .arg(close) + .arg(QRegularExpression::escape(model->name())) + .arg(close) + .arg(close)))) + model->setTemplateRegenerator(true); + } } } diff --git a/src/webots/vrml/WbProtoModel.hpp b/src/webots/vrml/WbProtoModel.hpp index f6de9c06d1a..065d81109e0 100644 --- a/src/webots/vrml/WbProtoModel.hpp +++ b/src/webots/vrml/WbProtoModel.hpp @@ -127,7 +127,8 @@ class WbProtoModel : public QObject { WbVersion mFileVersion; QString mName; QString mInfo; - bool mIsDeterministic; // i.e doesn't have the 'nonDeterministic' tag + bool mIsDeterministic; // i.e doesn't have the 'nonDeterministic' tag + bool mHasIndirectFieldAccess; // i.e. has the 'indirectFieldAccess' tag QList mFieldModels; QString mUrl; // how the PROTO is referenced diff --git a/src/webots/wren/WbWrenCamera.cpp b/src/webots/wren/WbWrenCamera.cpp index f507904b2d1..eedf0c47c48 100644 --- a/src/webots/wren/WbWrenCamera.cpp +++ b/src/webots/wren/WbWrenCamera.cpp @@ -452,7 +452,7 @@ void WbWrenCamera::render() { else if (mType == 's') materialName = "segmentation"; wr_scene_enable_depth_reset(wr_scene_get_instance(), false); - wr_scene_render_to_viewports(wr_scene_get_instance(), numActiveViewports, mViewportsToRender, materialName, true); + wr_scene_render_to_viewports(wr_scene_get_instance(), numActiveViewports, mViewportsToRender, materialName, true, false); if (!isPlanarProjection()) applySphericalPostProcessingEffect(); diff --git a/src/webots/wren/WbWrenPicker.cpp b/src/webots/wren/WbWrenPicker.cpp index 2b78ab90c75..742051aecc8 100644 --- a/src/webots/wren/WbWrenPicker.cpp +++ b/src/webots/wren/WbWrenPicker.cpp @@ -134,7 +134,7 @@ bool WbWrenPicker::pick(int x, int y) { wr_viewport_enable_skybox(mViewport, false); wr_scene_enable_translucence(scene, false); wr_scene_enable_depth_reset(scene, false); - wr_scene_render_to_viewports(scene, 1, &mViewport, "picking", true); + wr_scene_render_to_viewports(scene, 1, &mViewport, "picking", true, false); wr_scene_enable_depth_reset(scene, true); wr_viewport_enable_skybox(mViewport, true); wr_scene_enable_translucence(scene, true); diff --git a/src/wren/Scene.cpp b/src/wren/Scene.cpp index 90791423099..87258a3e4dc 100644 --- a/src/wren/Scene.cpp +++ b/src/wren/Scene.cpp @@ -224,7 +224,7 @@ namespace wren { debug::printSceneTree(); } - void Scene::render(bool culling) { + void Scene::render(bool culling, bool offScreen) { assert(glstate::isInitialized()); ++mFrameCounter; @@ -233,10 +233,10 @@ namespace wren { // debug::printCacheContents(); // debug::printSceneTree(); - renderToViewports({mMainViewport}, culling); + renderToViewports({mMainViewport}, culling, offScreen); } - void Scene::renderToViewports(const std::vector &viewports, bool culling) { + void Scene::renderToViewports(const std::vector &viewports, bool culling, bool offScreen) { assert(glstate::isInitialized()); DEBUG("Notify frame listeners..."); @@ -282,7 +282,7 @@ namespace wren { } } else { renderToViewport(culling); - if (mCurrentViewport == mMainViewport && mCurrentViewport->frameBuffer()) { + if (!offScreen && mCurrentViewport == mMainViewport && mCurrentViewport->frameBuffer()) { glstate::bindDrawFrameBuffer(0); mCurrentViewport->frameBuffer()->blit(0, true, false, false, 0, 0, 0, 0, 0, 0, mCurrentViewport->width() * mCurrentViewport->pixelRatio(), @@ -909,22 +909,23 @@ void wr_scene_terminate_frame_capture(WrScene *scene) { reinterpret_cast(scene)->terminateFrameCapture(); } -void wr_scene_render(WrScene *scene, const char *material_name, bool culling) { +void wr_scene_render(WrScene *scene, const char *material_name, bool culling, bool offScreen) { if (material_name) wren::Renderable::setUseMaterial(material_name); - reinterpret_cast(scene)->render(culling); + reinterpret_cast(scene)->render(culling, offScreen); wren::Renderable::setUseMaterial(NULL); } -void wr_scene_render_to_viewports(WrScene *scene, int count, WrViewport **viewports, const char *material_name, bool culling) { +void wr_scene_render_to_viewports(WrScene *scene, int count, WrViewport **viewports, const char *material_name, bool culling, + bool offScreen) { if (material_name) wren::Renderable::setUseMaterial(material_name); wren::Viewport **start = reinterpret_cast(viewports); std::vector viewportsVector(start, start + count); - reinterpret_cast(scene)->renderToViewports(viewportsVector, culling); + reinterpret_cast(scene)->renderToViewports(viewportsVector, culling, offScreen); wren::Renderable::setUseMaterial(NULL); } diff --git a/src/wren/Scene.hpp b/src/wren/Scene.hpp index d865cbd9043..9f68654b77e 100644 --- a/src/wren/Scene.hpp +++ b/src/wren/Scene.hpp @@ -90,8 +90,8 @@ namespace wren { int computeNodeCount() const; static void printSceneTree(); - void render(bool culling); - void renderToViewports(const std::vector &viewports, bool culling); + void render(bool culling, bool offScreen = false); + void renderToViewports(const std::vector &viewports, bool culling, bool offScreen = false); void addFrameListener(void (*listener)()) { mListeners.push_back(listener); } void removeFrameListener(void (*listener)()); diff --git a/src/wren/Viewport.cpp b/src/wren/Viewport.cpp index 1d5e25fdeeb..723477ebb10 100644 --- a/src/wren/Viewport.cpp +++ b/src/wren/Viewport.cpp @@ -180,10 +180,6 @@ namespace wren { } } - glstate::bindDrawFrameBuffer(0); - - clear(); - for (size_t i = 0; i < mPostProcessingEffects.size(); ++i) mPostProcessingEffects[i]->apply(); } @@ -202,9 +198,6 @@ namespace wren { mAmbientOcclusionEffect->setResultFrameBuffer(mFrameBuffer); mAmbientOcclusionEffect->firstPass()->setInputTexture(0, mFrameBuffer->outputTexture(0)); } - glstate::bindDrawFrameBuffer(0); - - clear(); mAmbientOcclusionEffect->apply(); } @@ -224,10 +217,6 @@ namespace wren { mAntiAliasingEffect->firstPass()->setInputTexture(0, mFrameBuffer->outputTexture(0)); } - glstate::bindDrawFrameBuffer(0); - - clear(); - mAntiAliasingEffect->apply(); } } diff --git a/src/wren/demo/default.cpp b/src/wren/demo/default.cpp index fad345389c1..1578317ef93 100644 --- a/src/wren/demo/default.cpp +++ b/src/wren/demo/default.cpp @@ -99,7 +99,7 @@ static void create_wren_scene() { // Render function. static void render() { - wr_scene_render(wr_scene_get_instance(), NULL, true); + wr_scene_render(wr_scene_get_instance(), NULL, true, false); glutSwapBuffers(); } diff --git a/src/wren/demo/phong.cpp b/src/wren/demo/phong.cpp index 7ed9807a5c3..f7facb5ce95 100644 --- a/src/wren/demo/phong.cpp +++ b/src/wren/demo/phong.cpp @@ -166,7 +166,7 @@ static void render() { static int i = 0; printf("\33[2K\rrendering iteration %d", i++); fflush(stdout); - wr_scene_render(wr_scene_get_instance(), NULL, true); + wr_scene_render(wr_scene_get_instance(), NULL, true, false); glutSwapBuffers(); } diff --git a/tests/physics/controllers/rolling_friction_supervisor/rolling_friction_supervisor.c b/tests/physics/controllers/rolling_friction_supervisor/rolling_friction_supervisor.c index 265c4a8bd90..4bcb50430b0 100644 --- a/tests/physics/controllers/rolling_friction_supervisor/rolling_friction_supervisor.c +++ b/tests/physics/controllers/rolling_friction_supervisor/rolling_friction_supervisor.c @@ -98,9 +98,9 @@ int main(int argc, char **argv) { // pre-registered velocities/positions after 200 timesteps (to ensure that if changes to the contact properties affect the // rolling behavior it will be detected) const double expected_positions[BALL_COUNT][3] = { - {-5.822856, -3.0, 0.498430}, {-14.564553, 0.0, 0.498392}, {-25.145860, 3.0, 1.942235}, - {-16.0, -8.0, 1.498430}, {-14.0, -8.0, 1.498430}, {-12.0, -8.0, 1.498430}, - {-5.822856, -27.511330, 0.498430}, {1.171054, -19.324655, 0.498430}, {1.171054, -13.936790, 0.498430}}; + {-5.822856, -3.0, 0.498430}, {-14.564553, 0.0, 0.498392}, {-25.145860, 3.0, 1.942235}, + {-16.0, -8.0, 1.498430}, {-14.0, -8.0, 1.498430}, {-12.0, -8.0, 1.498430}, + {-5.822856, -27.511330, 0.498430}, {-5.822856, -19.314704, 0.498430}, {-5.822856, -13.936790, 0.498430}}; const double expected_velocities[BALL_COUNT][6] = {{5.957204, 0.0, 0.0, 0.0, 11.951927, 0.0}, {4.164077, 0.0, 0.000304, 0.0, 8.355176, 0.0}, {1.817242, 0.0, -0.368373, 0.0, 3.719816, 0.0}, @@ -108,8 +108,8 @@ int main(int argc, char **argv) { {0.0, 0.0, 0.0, 0.0, 0.0, 6.164800}, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {5.957204, 3.686891, 0.0, -7.397003, 11.951927, -3.446233}, - {7.029157, 0.0, 0.0, 0.0, 0.0, -4.455340}, - {7.029157, 0.0, 0.0, 0.0, 0.0, -4.867729}}; + {5.957204, 0.0, 0.0, 0.0, 11.951927, -4.639518}, + {5.957204, 0.0, 0.0, 0.0, 11.951927, -4.867729}}; for (int i = 0; i < BALL_COUNT; ++i) { sprintf(name, "BALL_%d", i + 1); diff --git a/tests/protos/controllers/template_indirect_field_access/.gitignore b/tests/protos/controllers/template_indirect_field_access/.gitignore new file mode 100644 index 00000000000..7abe0a8a413 --- /dev/null +++ b/tests/protos/controllers/template_indirect_field_access/.gitignore @@ -0,0 +1 @@ +/template_indirect_field_access diff --git a/tests/protos/controllers/template_indirect_field_access/Makefile b/tests/protos/controllers/template_indirect_field_access/Makefile new file mode 100644 index 00000000000..9734e04d8e9 --- /dev/null +++ b/tests/protos/controllers/template_indirect_field_access/Makefile @@ -0,0 +1,19 @@ +# Copyright 1996-2023 Cyberbotics Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +### Do not modify: this includes Webots global Makefile.include +null := +space := $(null) $(null) +WEBOTS_HOME_PATH?=$(subst $(space),\ ,$(strip $(subst \,/,$(WEBOTS_HOME)))) +include $(WEBOTS_HOME_PATH)/resources/Makefile.include diff --git a/tests/protos/controllers/template_indirect_field_access/template_indirect_field_access.c b/tests/protos/controllers/template_indirect_field_access/template_indirect_field_access.c new file mode 100644 index 00000000000..6a5605d5473 --- /dev/null +++ b/tests/protos/controllers/template_indirect_field_access/template_indirect_field_access.c @@ -0,0 +1,19 @@ +#include +#include +#include + +#include "../../../lib/ts_assertion.h" +#include "../../../lib/ts_utils.h" + +#define TIME_STEP 32 + +int main(int argc, char **argv) { + ts_setup(argv[1]); // give the controller args + WbNodeRef proto = wb_supervisor_node_get_from_def("PROTO_template_indirect_field_access"); + WbFieldRef radarCrossSection = wb_supervisor_node_get_proto_field(proto, "radarCrossSection"); + ts_assert_double_in_delta(wb_supervisor_field_get_sf_float(radarCrossSection), 6.0, 0.0001, + "radarCrossSection should be 6.0"); + ts_send_success(); + + return EXIT_SUCCESS; +} diff --git a/tests/protos/protos/TemplateIndirectFieldAccess.proto b/tests/protos/protos/TemplateIndirectFieldAccess.proto new file mode 100644 index 00000000000..b115bed1463 --- /dev/null +++ b/tests/protos/protos/TemplateIndirectFieldAccess.proto @@ -0,0 +1,18 @@ +#VRML_SIM R2024a utf8 +# tags: indirectFieldAccess +# template language: javascript + +PROTO TemplateIndirectFieldAccess [ + unconnectedField SFFloat field1 1 + unconnectedField SFFloat field2 5 +] +{ + Solid { + %< + function sum(fieldsObj) { + return fieldsObj.field1.value + fieldsObj.field2.value; + } + >% + radarCrossSection %<= sum(fields) >% + } +} diff --git a/tests/protos/protos/TemplateIndirectFieldAccessLua.proto b/tests/protos/protos/TemplateIndirectFieldAccessLua.proto new file mode 100644 index 00000000000..d6b9a0c20e3 --- /dev/null +++ b/tests/protos/protos/TemplateIndirectFieldAccessLua.proto @@ -0,0 +1,17 @@ +#VRML_SIM R2024a utf8 +# tags: indirectFieldAccess + +PROTO TemplateIndirectFieldAccessLua [ + unconnectedField SFFloat field1 1 + unconnectedField SFFloat field2 5 +] +{ + Solid { + %{ + function sum(fieldsObj) + return fieldsObj.field1.value + fieldsObj.field2.value; + end + }% + radarCrossSection %{= sum(fields) }% + } +} diff --git a/tests/protos/worlds/template_indirect_field_access.wbt b/tests/protos/worlds/template_indirect_field_access.wbt new file mode 100644 index 00000000000..1d79b67a155 --- /dev/null +++ b/tests/protos/worlds/template_indirect_field_access.wbt @@ -0,0 +1,39 @@ +#VRML_SIM R2024a utf8 + +EXTERNPROTO "webots://tests/protos/protos/TemplateIndirectFieldAccess.proto" +EXTERNPROTO "webots://tests/default/protos/TestSuiteEmitter.proto" +EXTERNPROTO "webots://tests/default/protos/TestSuiteSupervisor.proto" + +WorldInfo { + coordinateSystem "NUE" + lineScale 1 +} +Viewpoint { + orientation -0.8999515417256047 0.42450878383113183 0.0993957493855782 0.509061 + position 0.303563 0.46348 0.936228 +} +Background { + skyColor [ + 0 0.811765 0.992157 + ] +} +DirectionalLight { + direction 0 0.5 -1 +} +Robot { + translation 0.1 0 0.15 + rotation 0 1 0 1.5708 + children [ + DEF PROTO_template_indirect_field_access TemplateIndirectFieldAccess { + } + TestSuiteEmitter { + } + ] + controller "template_indirect_field_access" + controllerArgs [ + "template_indirect_field_access" + ] + supervisor TRUE +} +TestSuiteSupervisor { +} diff --git a/tests/protos/worlds/template_indirect_field_access_lua.wbt b/tests/protos/worlds/template_indirect_field_access_lua.wbt new file mode 100644 index 00000000000..b16b039f496 --- /dev/null +++ b/tests/protos/worlds/template_indirect_field_access_lua.wbt @@ -0,0 +1,39 @@ +#VRML_SIM R2024a utf8 + +EXTERNPROTO "webots://tests/protos/protos/TemplateIndirectFieldAccessLua.proto" +EXTERNPROTO "webots://tests/default/protos/TestSuiteEmitter.proto" +EXTERNPROTO "webots://tests/default/protos/TestSuiteSupervisor.proto" + +WorldInfo { + coordinateSystem "NUE" + lineScale 1 +} +Viewpoint { + orientation -0.8999515417256047 0.42450878383113183 0.0993957493855782 0.509061 + position 0.303563 0.46348 0.936228 +} +Background { + skyColor [ + 0 0.811765 0.992157 + ] +} +DirectionalLight { + direction 0 0.5 -1 +} +Robot { + translation 0.1 0 0.15 + rotation 0 1 0 1.5708 + children [ + DEF PROTO_template_indirect_field_access TemplateIndirectFieldAccessLua { + } + TestSuiteEmitter { + } + ] + controller "template_indirect_field_access" + controllerArgs [ + "template_indirect_field_access (lua)" + ] + supervisor TRUE +} +TestSuiteSupervisor { +}