From c4ac8a9f06d91637336878e5823d3ae49682aef5 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 4 Mar 2021 18:57:22 -0800 Subject: [PATCH] AutomationEngine can now be used as a remote control. --- android/filament-android/libfilament-jni.map | 2 + .../src/main/cpp/AutomationEngine.cpp | 32 +++- .../filament/utils/AutomationEngine.java | 65 ++++++-- .../android/filament/utils/RemoteServer.java | 5 +- .../android/filament/gltf/MainActivity.kt | 27 ++-- filament/include/filament/LightManager.h | 2 +- libs/viewer/include/viewer/AutomationEngine.h | 47 ++++-- libs/viewer/include/viewer/RemoteServer.h | 5 +- libs/viewer/include/viewer/Settings.h | 19 +++ libs/viewer/include/viewer/SimpleViewer.h | 14 +- libs/viewer/src/AutomationEngine.cpp | 18 ++- libs/viewer/src/Settings.cpp | 145 +++++++++++++++++- libs/viewer/src/SimpleViewer.cpp | 77 +++++----- web/samples/remote.html | 59 +++---- 14 files changed, 378 insertions(+), 139 deletions(-) diff --git a/android/filament-android/libfilament-jni.map b/android/filament-android/libfilament-jni.map index e573ed2491..b87c1001cb 100644 --- a/android/filament-android/libfilament-jni.map +++ b/android/filament-android/libfilament-jni.map @@ -3,6 +3,7 @@ LIBFILAMENT { Java_com_google_android_filament_*; JNI*; *filament*Camera*; + *filament*Color*; *filament*Exposure*; *filament*Skybox*; *filament*Engine*; @@ -11,6 +12,7 @@ LIBFILAMENT { *filament*IndirectLight*; *filament*LightManager*; *filament*Renderer*; + *filament*Scene*; *filament*Transform*; *filament*Material*; *filament*IndexBuffer*; diff --git a/android/filament-utils-android/src/main/cpp/AutomationEngine.cpp b/android/filament-utils-android/src/main/cpp/AutomationEngine.cpp index a805db4565..cd0029b5fa 100644 --- a/android/filament-utils-android/src/main/cpp/AutomationEngine.cpp +++ b/android/filament-utils-android/src/main/cpp/AutomationEngine.cpp @@ -20,6 +20,7 @@ using namespace filament; using namespace filament::viewer; +using namespace utils; extern "C" JNIEXPORT jlong JNICALL Java_com_google_android_filament_utils_AutomationEngine_nCreateAutomationEngine(JNIEnv* env, jclass, @@ -33,7 +34,7 @@ Java_com_google_android_filament_utils_AutomationEngine_nCreateAutomationEngine( extern "C" JNIEXPORT jlong JNICALL Java_com_google_android_filament_utils_AutomationEngine_nCreateDefaultAutomationEngine(JNIEnv* env, jclass klass) { - return (jlong) AutomationEngine::createDefaultTest(); + return (jlong) AutomationEngine::createDefault(); } extern "C" JNIEXPORT void JNICALL @@ -90,6 +91,35 @@ Java_com_google_android_filament_utils_AutomationEngine_nTick(JNIEnv* env, jclas } } +extern "C" JNIEXPORT void JNICALL +Java_com_google_android_filament_utils_AutomationEngine_nApplySettings(JNIEnv* env, jclass klass, + jlong nativeAutomation, jstring json, jlong view, jlongArray materials, jlong nativeIbl, + jint lightEntity, jlong nativeLm, jlong scene) { + using MaterialPointer = MaterialInstance*; + jsize materialCount = 0; + jlong* longMaterials = nullptr; + MaterialPointer* ptrMaterials = nullptr; + if (materials) { + materialCount = env->GetArrayLength(materials); + ptrMaterials = new MaterialPointer[materialCount]; + longMaterials = env->GetLongArrayElements(materials, nullptr); + for (jsize i = 0; i < materialCount; i++) { + ptrMaterials[i] = (MaterialPointer) longMaterials[i]; + } + } + AutomationEngine* automation = (AutomationEngine*) nativeAutomation; + const char* nativeJson = env->GetStringUTFChars(json, 0); + size_t jsonLength = env->GetStringUTFLength(json); + automation->applySettings(nativeJson, jsonLength, (View*) view, ptrMaterials, materialCount, + (IndirectLight*) nativeIbl, (Entity&) lightEntity, (LightManager*) nativeLm, + (Scene*) scene); + env->ReleaseStringUTFChars(json, nativeJson); + if (longMaterials) { + env->ReleaseLongArrayElements(materials, longMaterials, 0); + delete[] ptrMaterials; + } +} + extern "C" JNIEXPORT void JNICALL Java_com_google_android_filament_utils_AutomationEngine_nSignalBatchMode(JNIEnv*, jclass, jlong native) { AutomationEngine* automation = (AutomationEngine*) native; diff --git a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/AutomationEngine.java b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/AutomationEngine.java index 90577f0ef8..82f030ddfc 100644 --- a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/AutomationEngine.java +++ b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/AutomationEngine.java @@ -19,20 +19,26 @@ package com.google.android.filament.utils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.google.android.filament.Entity; +import com.google.android.filament.IndirectLight; +import com.google.android.filament.LightManager; +import com.google.android.filament.Scene; import com.google.android.filament.View; import com.google.android.filament.MaterialInstance; import com.google.android.filament.Renderer; /** - * The AutomationEngine iterates through settings permutations and pushes them to Filament. + * The AutomationEngine makes it easy to push a bag of settings values to Filament. + * It can also be used to iterate through settings permutations for testing purposes. * - * Upon construction, automation is given a JSON string that tells it how to generate permutations - * of settings. Automation is always in one of two states: running or idle. The running state can be - * entered immediately (startRunning) or by requesting batch mode (startBatchMode). + * When creating an automation engine for testing purposes, clients give it a JSON string that + * tells it how to generate permutations of settings. Automation is always in one of two states: + * running or idle. The running state can be entered immediately (startRunning) or by requesting + * batch mode (startBatchMode). * - * Clients must call tick() after each frame is rendered, which provides an opportunity to push - * settings to Filament, increment the current test index (if enough time has elapsed), and request - * an asychronous screenshot. + * When executing a test, clients should call tick() after each frame is rendered, which provides an + * opportunity to push settings to Filament, increment the current test index (if enough time has + * elapsed), and request an asychronous screenshot. * * The time to sleep between tests is configurable and can be set to zero. Automation also waits a * specified minimum number of frames between tests. @@ -81,7 +87,8 @@ public class AutomationEngine { } /** - * Creates an automation engine for the default test sequence. + * Creates an automation engine for the sole purpose of pushing settings, or for executing + * the default test sequence. * * To see how the default test sequence is generated, search for DEFAULT_AUTOMATION. */ @@ -99,20 +106,22 @@ public class AutomationEngine { } /** - * Activates automation. During the subsequent call to tick(), the first test is applied - * and automation enters the running state. + * Activates the automation test. During the subsequent call to tick(), the first test is + * applied and automation enters the running state. */ public void startRunning() { nStartRunning(mNativeObject); } /** - * Activates automation, but enters a paused state until the user calls signalBatchMode(). + * Activates the automation test, but enters a paused state until the user calls + * signalBatchMode(). */ public void startBatchMode() { nStartBatchMode(mNativeObject); } /** * Notifies the automation engine that time has passed and a new frame has been rendered. * - * This is when settings get applied, screenshots are (optionally) exported, etc. + * This is when settings get applied, screenshots are (optionally) exported, and the internal + * test counter is potentially incremented. * * @param view The Filament View that automation pushes changes to. * @param materials Optional set of of materials that can receive parameter tweaks. @@ -133,6 +142,30 @@ public class AutomationEngine { nTick(mNativeObject, nativeView, nativeMaterialInstances, nativeRenderer, deltaTime); } + /** + * Mutates a set of client-owned Filament objects according to a JSON string. + * + * This method is an alternative to tick(). It allows clients to use the automation engine as a + * remote control, as opposed to iterating through a predetermined test sequence. + */ + public void applySettings(@NonNull String settingsJson, @NonNull View view, + @Nullable MaterialInstance[] materials, @Nullable IndirectLight ibl, @Entity int light, + @NonNull LightManager lm, @NonNull Scene scene) { + long[] nativeMaterialInstances = null; + if (materials != null) { + nativeMaterialInstances = new long[materials.length]; + for (int i = 0; i < nativeMaterialInstances.length; i++) { + nativeMaterialInstances[i] = materials[i].getNativeObject(); + } + } + long nativeView = view.getNativeObject(); + long nativeIbl = ibl == null ? 0 : ibl.getNativeObject(); + long nativeLm = lm.getNativeObject(); + long nativeScene = scene.getNativeObject(); + nApplySettings(mNativeObject, settingsJson, nativeView, nativeMaterialInstances, + nativeIbl, light, nativeLm, nativeScene); + } + /** * Signals that batch mode can begin. Call this after all meshes and textures finish loading. */ @@ -156,10 +189,14 @@ public class AutomationEngine { private static native long nCreateAutomationEngine(String jsonSpec); private static native long nCreateDefaultAutomationEngine(); - private static native void nSetOptions(long nativeObject, float sleepDuration, int minFrameCount, boolean verbose); + private static native void nSetOptions(long nativeObject, float sleepDuration, + int minFrameCount, boolean verbose); private static native void nStartRunning(long nativeObject); private static native void nStartBatchMode(long nativeObject); - private static native void nTick(long nativeObject, long view, long[] materials, long renderer, float deltaTime); + private static native void nTick(long nativeObject, long view, long[] materials, long renderer, + float deltaTime); + private static native void nApplySettings(long nativeObject, String jsonSettings, long view, + long[] materials, long ibl, int light, long lightManager, long scene); private static native void nSignalBatchMode(long nativeObject); private static native void nStopRunning(long nativeObject); private static native boolean nShouldClose(long nativeObject); diff --git a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/RemoteServer.java b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/RemoteServer.java index 01ebfad079..e4c3b95889 100644 --- a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/RemoteServer.java +++ b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/RemoteServer.java @@ -24,11 +24,8 @@ import java.nio.ByteOrder; /** * Manages a tiny WebSocket server that can receive model data and viewer settings. * - * Client apps can call acquireReceivedMessage to check for a new model and pop it off the small + * Client apps can call acquireReceivedMessage to check for new data and pop it off the small * internal queue. - * - * TODO: Currently this can only receive model data. We would like to extend it to receive - * viewer settings and commands (e.g. "Start Automation Test"). */ public class RemoteServer { private long mNativeObject; diff --git a/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt b/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt index 48a5bf5ada..e54f9095a7 100644 --- a/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt +++ b/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt @@ -21,20 +21,18 @@ import android.app.Activity import android.os.Bundle import android.util.Log import android.view.* +import android.view.GestureDetector import android.widget.TextView import android.widget.Toast -import com.google.android.filament.utils.KtxLoader -import com.google.android.filament.utils.ModelViewer -import com.google.android.filament.utils.RemoteServer -import com.google.android.filament.utils.Utils +import com.google.android.filament.utils.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.ByteArrayInputStream -import java.io.IOException import java.nio.Buffer import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets import java.util.zip.ZipInputStream class MainActivity : Activity() { @@ -55,6 +53,7 @@ class MainActivity : Activity() { private var remoteServer: RemoteServer? = null private var statusToast: Toast? = null private var statusText: String? = null + private val automation = AutomationEngine() @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { @@ -228,21 +227,19 @@ class MainActivity : Activity() { clearStatusText() titlebarHint.text = message.label CoroutineScope(Dispatchers.IO).launch { - try { - if (message.label.endsWith(".zip")) { - loadZip(message) - } else { - loadGlb(message) - } - } catch (exc: IOException) { - setStatusText("URL fetch failed.") - Log.e(TAG, "URL fetch failed", exc) + if (message.label.endsWith(".zip")) { + loadZip(message) + } else { + loadGlb(message) } } } fun loadSettings(message: RemoteServer.ReceivedMessage) { - Log.i(TAG, "Downloaded settings JSON (${message.buffer.capacity()} bytes)") + val json = StandardCharsets.UTF_8.decode(message.buffer).toString() + automation.applySettings(json, modelViewer.view, null, + modelViewer.scene.indirectLight, modelViewer.light, modelViewer.engine.lightManager, + modelViewer.scene) } inner class FrameCallback : Choreographer.FrameCallback { diff --git a/filament/include/filament/LightManager.h b/filament/include/filament/LightManager.h index cb3bfbc78a..b5ee39305e 100644 --- a/filament/include/filament/LightManager.h +++ b/filament/include/filament/LightManager.h @@ -315,7 +315,7 @@ public: * @warning This API is still experimental and subject to change. * @see View::setShadowType */ - struct { + struct Vsm { /** * The number of MSAA samples to use when rendering VSM shadow maps. * Must be a power-of-two and greater than or equal to 1. A value of 1 effectively turns diff --git a/libs/viewer/include/viewer/AutomationEngine.h b/libs/viewer/include/viewer/AutomationEngine.h index 16f0b31bf3..ca70a33509 100644 --- a/libs/viewer/include/viewer/AutomationEngine.h +++ b/libs/viewer/include/viewer/AutomationEngine.h @@ -21,6 +21,7 @@ namespace filament { +class LightManager; class MaterialInstance; class Renderer; class View; @@ -28,16 +29,16 @@ class View; namespace viewer { /** - * The AutomationEngine iterates through settings permutations and pushes them to Filament, - * optionally exporting screenshots along the way. + * The AutomationEngine makes it easy to push a bag of settings values to Filament. + * It can also be used to iterate through settings permutations for testing purposes. * - * Upon construction, automation is given an immutable reference to an AutomationSpec. It is - * always in one of two states: running or idle. The running state can be entered immediately - * (startRunning) or by requesting batch mode (startBatchMode). + * When creating an automation engine for testing purposes, clients give it an immutable reference + * to an AutomationSpec. It is always in one of two states: running or idle. The running state can + * be entered immediately (startRunning) or by requesting batch mode (startBatchMode). * - * Clients must call tick() after each frame is rendered, which gives automation an opportunity to - * push settings to Filament, increment the current test index (if enough time has elapsed), and - * request an asychronous screenshot. + * When executing a test, clients should call tick() after each frame is rendered, which gives + * automation an opportunity to push settings to Filament, increment the current test index (if + * enough time has elapsed), and request an asychronous screenshot. * * The time to sleep between tests is configurable and can be set to zero. Automation also waits a * specified minimum number of frames between tests. @@ -111,29 +112,30 @@ public: static AutomationEngine* createFromJSON(const char* jsonSpec, size_t size); /** - * Creates an automation engine for the default test sequence. + * Creates an automation engine for the sole purpose of pushing settings, or for executing + * the default test sequence. * * To see how the default test sequence is generated, search for DEFAULT_AUTOMATION. */ - static AutomationEngine* createDefaultTest(); - - ~AutomationEngine(); + static AutomationEngine* createDefault(); /** - * Activates automation. During the subsequent call to tick(), the first test is applied - * and automation enters the running state. + * Activates the automation test. During the subsequent call to tick(), the first test is + * applied and automation enters the running state. */ void startRunning(); /** - * Activates automation, but enters a paused state until the user calls signalBatchMode(). + * Activates the automation test, but enters a paused state until the user calls + * signalBatchMode(). */ void startBatchMode(); /** - * Notifies the automation engine that time has passed and a new frame has been rendered. + * Notifies the automation engine that time has passed, a new frame has been rendered. * - * This is when settings get applied, screenshots are (optionally) exported, etc. + * This is when settings get applied, screenshots are (optionally) exported, and the internal + * test counter is potentially incremented. * * @param view The Filament View that automation pushes changes to. * @param materials An optional set of of materials that can receive parameter tweaks. @@ -144,6 +146,16 @@ public: void tick(View* view, MaterialInstance* const* materials, size_t materialCount, Renderer* renderer, float deltaTime); + /** + * Mutates a set of client-owned Filament objects according to a JSON string. + * + * This method is an alternative to tick(). It allows clients to use the automation engine as a + * remote control, as opposed to iterating through a predetermined test sequence. + */ + void applySettings(const char* json, size_t jsonLength, View* view, + MaterialInstance* const* materials, size_t materialCount, IndirectLight* ibl, + utils::Entity sunlight, LightManager* lm, Scene* scene); + /** * Signals that batch mode can begin. Call this after all meshes and textures finish loading. */ @@ -184,6 +196,7 @@ public: size_t testCount() const { return mSpec->size(); } bool isBatchModeEnabled() const { return mBatchModeEnabled; } const char* getStatusMessage() const; + ~AutomationEngine(); private: AutomationSpec const * const mSpec; diff --git a/libs/viewer/include/viewer/RemoteServer.h b/libs/viewer/include/viewer/RemoteServer.h index c7ab5dd7e2..cdccf28812 100644 --- a/libs/viewer/include/viewer/RemoteServer.h +++ b/libs/viewer/include/viewer/RemoteServer.h @@ -46,12 +46,9 @@ struct ReceivedMessage { /** * Manages a tiny WebSocket server that can receive model data and viewer settings. * - * Client apps can call peekReceivedMessage to check for a new model, or acquireReceivedMessage + * Client apps can call peekReceivedMessage to check for new data, or acquireReceivedMessage * to pop it off the small internal queue. When they are done examining the message contents * they should call releaseReceivedMessage. - * - * TODO: Currently this can only receive model data. We would like to extend it to receive - * viewer settings and commands (e.g. "Start Automation Test"). */ class RemoteServer { public: diff --git a/libs/viewer/include/viewer/Settings.h b/libs/viewer/include/viewer/Settings.h index 9b5d076c65..8119680636 100644 --- a/libs/viewer/include/viewer/Settings.h +++ b/libs/viewer/include/viewer/Settings.h @@ -18,7 +18,10 @@ #define VIEWER_SETTINGS_H #include +#include +#include #include +#include #include #include @@ -37,6 +40,7 @@ struct DynamicLightingSettings; struct MaterialSettings; struct Settings; struct ViewSettings; +struct LightSettings; using AmbientOcclusionOptions = filament::View::AmbientOcclusionOptions; using AntiAliasing = filament::View::AntiAliasing; @@ -50,10 +54,13 @@ using TemporalAntiAliasingOptions = filament::View::TemporalAntiAliasingOptions; using ToneMapping = filament::ColorGrading::ToneMapping; using VignetteOptions = filament::View::VignetteOptions; using VsmShadowOptions = filament::View::VsmShadowOptions; +using LightManager = filament::LightManager; // These functions push all editable property values to their respective Filament objects. void applySettings(const ViewSettings& settings, View* dest); void applySettings(const MaterialSettings& settings, MaterialInstance* dest); +void applySettings(const LightSettings& settings, IndirectLight* ibl, utils::Entity sunlight, + LightManager* lm, Scene* scene); // Creates a new ColorGrading object based on the given settings. ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* engine); @@ -139,9 +146,21 @@ struct MaterialSettings { MaterialProperty float4[MAX_COUNT]; }; +struct LightSettings { + bool enableShadows = true; + bool enableSunlight = true; + LightManager::ShadowOptions shadowOptions; + float sunlightIntensity = 100000.0f; + math::float3 sunlightDirection = {0.6, -1.0, -0.8};; + math::float3 sunlightColor = filament::Color::toLinear({ 0.98, 0.92, 0.89}); + float iblIntensity = 30000.0f; + float iblRotation = 0.0f; +}; + struct Settings { ViewSettings view; MaterialSettings material; + LightSettings lighting; }; } // namespace viewer diff --git a/libs/viewer/include/viewer/SimpleViewer.h b/libs/viewer/include/viewer/SimpleViewer.h index a84fccbfcd..9c9573c8f3 100644 --- a/libs/viewer/include/viewer/SimpleViewer.h +++ b/libs/viewer/include/viewer/SimpleViewer.h @@ -146,7 +146,7 @@ public: * Enables a built-in light source (useful for creating shadows). * Defaults to true. */ - void enableSunlight(bool b) { mEnableSunlight = b; } + void enableSunlight(bool b) { mSettings.lighting.enableSunlight = b; } /** * Enables dithering on the view. @@ -187,7 +187,7 @@ public: * See also filament::IndirectLight::setIntensity(). * Defaults to 30000.0. */ - void setIBLIntensity(float brightness) { mIblIntensity = brightness; } + void setIBLIntensity(float brightness) { mSettings.lighting.iblIntensity = brightness; } /** * Gets a modifiable reference to stashed state. @@ -217,18 +217,8 @@ private: // Properties that can be changed from the UI. int mCurrentAnimation = 1; bool mResetAnimation = true; - float mIblIntensity = 30000.0f; - float mIblRotation = 0.0f; - float mSunlightIntensity = 100000.0f; // <-- This value is overridden when loading an IBL. - filament::math::float3 mSunlightColor = filament::Color::toLinear({ 0.98, 0.92, 0.89}); - filament::math::float3 mSunlightDirection = {0.6, -1.0, -0.8}; bool mEnableWireframe = false; - bool mEnableSunlight = true; - bool mEnableShadows = true; - int mShadowCascades = 1; - bool mEnableContactShadows = false; int mVsmMsaaSamplesLog2 = 1; - std::array mSplitPositions = {0.25f, 0.50f, 0.75f}; Settings mSettings; int mSidebarWidth; uint32_t mFlags; diff --git a/libs/viewer/src/AutomationEngine.cpp b/libs/viewer/src/AutomationEngine.cpp index 4e95a0cfbe..7b02014ea8 100644 --- a/libs/viewer/src/AutomationEngine.cpp +++ b/libs/viewer/src/AutomationEngine.cpp @@ -89,7 +89,7 @@ AutomationEngine* AutomationEngine::createFromJSON(const char* jsonSpec, size_t return result; } -AutomationEngine* AutomationEngine::createDefaultTest() { +AutomationEngine* AutomationEngine::createDefault() { AutomationSpec* spec = AutomationSpec::generateDefaultTestCases(); if (!spec) { return nullptr; @@ -132,15 +132,27 @@ void AutomationEngine::exportSettings(const Settings& settings, const char* file gStatus = "Exported to '" + std::string(filename) + "' in the current folder."; } +void AutomationEngine::applySettings(const char* json, size_t jsonLength, View* view, + MaterialInstance* const* materials, size_t materialCount, IndirectLight* ibl, + utils::Entity sunlight, LightManager* lm, Scene* scene) { + JsonSerializer serializer; + serializer.readJson(json, jsonLength, mSettings); + viewer::applySettings(mSettings->view, view); + for (size_t i = 0; i < materialCount; i++) { + viewer::applySettings(mSettings->material, materials[i]); + } + viewer::applySettings(mSettings->lighting, ibl, sunlight, lm, scene); +} + void AutomationEngine::tick(View* view, MaterialInstance* const* materials, size_t materialCount, Renderer* renderer, float deltaTime) { const auto activateTest = [this, view, materials, materialCount]() { mElapsedTime = 0; mElapsedFrames = 0; mSpec->get(mCurrentTest, mSettings); - applySettings(mSettings->view, view); + viewer::applySettings(mSettings->view, view); for (size_t i = 0; i < materialCount; i++) { - applySettings(mSettings->material, materials[i]); + viewer::applySettings(mSettings->material, materials[i]); } if (mOptions.verbose) { utils::slog.i << "Running test " << mCurrentTest << utils::io::endl; diff --git a/libs/viewer/src/Settings.cpp b/libs/viewer/src/Settings.cpp index 62d5fad1ce..139a0ea00d 100644 --- a/libs/viewer/src/Settings.cpp +++ b/libs/viewer/src/Settings.cpp @@ -20,6 +20,8 @@ #include "jsonParseUtils.h" +#include + #include #include @@ -38,6 +40,7 @@ static std::string writeJson(const DepthOfFieldOptions& in); static std::string writeJson(const DynamicLightingSettings& in); static std::string writeJson(const FogOptions& in); static std::string writeJson(const MaterialSettings& in); +static std::string writeJson(const LightSettings& in); static std::string writeJson(const RenderQuality& in); static std::string writeJson(const TemporalAntiAliasingOptions& in); static std::string writeJson(const ViewSettings& in); @@ -660,6 +663,92 @@ static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Material return i; } +static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, + LightManager::ShadowOptions::Vsm* out) { + CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i++].size; + for (int j = 0; j < size; ++j) { + const jsmntok_t tok = tokens[i]; + CHECK_KEY(tok); + if (compare(tok, jsonChunk, "msaaSamples") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->msaaSamples); + } else { + slog.w << "Invalid shadow options VSM key: '" << STR(tok, jsonChunk) << "'" << io::endl; + i = parse(tokens, i + 1); + } + if (i < 0) { + slog.e << "Invalid shadow options VSM value: '" << STR(tok, jsonChunk) << "'" << io::endl; + return i; + } + } + return i; +} + +static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, + LightManager::ShadowOptions* out) { + CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i++].size; + math::float3 splitsVector; + for (int j = 0; j < size; ++j) { + const jsmntok_t tok = tokens[i]; + CHECK_KEY(tok); + if (compare(tok, jsonChunk, "screenSpaceContactShadows") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->screenSpaceContactShadows); + } else if (compare(tok, jsonChunk, "shadowCascades") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->shadowCascades); + } else if (compare(tok, jsonChunk, "cascadeSplitPositions") == 0) { + i = parse(tokens, i + 1, jsonChunk, &splitsVector); + out->cascadeSplitPositions[0] = splitsVector[0]; + out->cascadeSplitPositions[1] = splitsVector[1]; + out->cascadeSplitPositions[2] = splitsVector[2]; + } else if (compare(tok, jsonChunk, "vsm") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->vsm); + } else { + slog.w << "Invalid shadow options key: '" << STR(tok, jsonChunk) << "'" << io::endl; + i = parse(tokens, i + 1); + } + if (i < 0) { + slog.e << "Invalid shadow options value: '" << STR(tok, jsonChunk) << "'" << io::endl; + return i; + } + } + return i; +} + +static int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, LightSettings* out) { + CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); + int size = tokens[i++].size; + for (int j = 0; j < size; ++j) { + const jsmntok_t tok = tokens[i]; + CHECK_KEY(tok); + if (compare(tok, jsonChunk, "enableShadows") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->enableShadows); + } else if (compare(tok, jsonChunk, "enableSunlight") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->enableSunlight); + } else if (compare(tok, jsonChunk, "shadowOptions") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->shadowOptions); + } else if (compare(tok, jsonChunk, "sunlightIntensity") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->sunlightIntensity); + } else if (compare(tok, jsonChunk, "sunlightDirection") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->sunlightDirection); + } else if (compare(tok, jsonChunk, "sunlightColor") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->sunlightColor); + } else if (compare(tok, jsonChunk, "iblIntensity") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->iblIntensity); + } else if (compare(tok, jsonChunk, "iblRotation") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->iblRotation); + } else { + slog.w << "Invalid light setting key: '" << STR(tok, jsonChunk) << "'" << io::endl; + i = parse(tokens, i + 1); + } + if (i < 0) { + slog.e << "Invalid light setting value: '" << STR(tok, jsonChunk) << "'" << io::endl; + return i; + } + } + return i; +} + int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out) { CHECK_TOKTYPE(tokens[i], JSMN_OBJECT); int size = tokens[i++].size; @@ -670,6 +759,8 @@ int parse(jsmntok_t const* tokens, int i, const char* jsonChunk, Settings* out) i = parse(tokens, i + 1, jsonChunk, &out->view); } else if (compare(tok, jsonChunk, "material") == 0) { i = parse(tokens, i + 1, jsonChunk, &out->material); + } else if (compare(tok, jsonChunk, "lighting") == 0) { + i = parse(tokens, i + 1, jsonChunk, &out->lighting); } else { slog.w << "Invalid group key: '" << STR(tok, jsonChunk) << "'" << io::endl; i = parse(tokens, i + 1); @@ -713,6 +804,27 @@ void applySettings(const MaterialSettings& settings, MaterialInstance* dest) { for (auto prop : settings.float4) { apply(prop, dest); } } +void applySettings(const LightSettings& settings, IndirectLight* ibl, utils::Entity sunlight, + LightManager* lm, Scene* scene) { + auto light = lm->getInstance(sunlight); + if (light) { + if (settings.enableSunlight) { + scene->addEntity(sunlight); + } else { + scene->remove(sunlight); + } + lm->setIntensity(light, settings.sunlightIntensity); + lm->setDirection(light, normalize(settings.sunlightDirection)); + lm->setColor(light, settings.sunlightColor); + lm->setShadowCaster(light, settings.enableShadows); + lm->setShadowOptions(light, settings.shadowOptions); + } + if (ibl) { + ibl->setIntensity(settings.iblIntensity); + ibl->setRotation(math::mat3f::rotation(settings.iblRotation, math::float3 { 0, 1, 0 })); + } +} + ColorGrading* createColorGrading(const ColorGradingSettings& settings, Engine* engine) { return ColorGrading::Builder() .quality(settings.quality) @@ -829,7 +941,8 @@ std::string writeJson(const Settings& in) { std::ostringstream oss; oss << "{\n" << "\"view\": " << writeJson(in.view) << ",\n" - << "\"material\": " << writeJson(in.material) + << "\"material\": " << writeJson(in.material) << ",\n" + << "\"lighting\": " << writeJson(in.lighting) << "}"; return oss.str(); } @@ -938,6 +1051,21 @@ std::string writeJson(const FogOptions& in) { return oss.str(); } +std::string writeJson(const LightManager::ShadowOptions& in) { + std::ostringstream oss; + const float* splits = in.cascadeSplitPositions; + math::float3 splitsVector = { splits[0], splits[1], splits[2] }; + oss << "{\n" + << "\"vsm\": {\n" + << "\"msaaSamples\": " << writeJson(in.vsm.msaaSamples) << "\n" + << "},\n" + << "\"screenSpaceContactShadows\": " << writeJson(in.screenSpaceContactShadows) << ",\n" + << "\"shadowCascades\": " << writeJson(in.shadowCascades) << ",\n" + << "\"cascadeSplitPositions\": " << writeJson(splitsVector) << "\n" + << "}"; + return oss.str(); +} + template static void writeJson(MaterialProperty prop, std::ostringstream& oss) { if (!prop.name.empty()) { @@ -978,6 +1106,21 @@ std::string writeJson(const MaterialSettings& in) { return result; } +std::string writeJson(const LightSettings& in) { + std::ostringstream oss; + oss << "{\n" + << "\"enableShadows\": " << writeJson(in.enableShadows) << ",\n" + << "\"enableSunlight\": " << writeJson(in.enableSunlight) << ",\n" + << "\"shadowOptions\": " << writeJson(in.shadowOptions) << ",\n" + << "\"sunlightIntensity\": " << writeJson(in.sunlightIntensity) << ",\n" + << "\"sunlightDirection\": " << writeJson(in.sunlightDirection) << ",\n" + << "\"sunlightColor\": " << writeJson(in.sunlightColor) << ",\n" + << "\"iblIntensity\": " << writeJson(in.iblIntensity) << ",\n" + << "\"iblRotation\": " << writeJson(in.iblRotation) << "\n" + << "}"; + return oss.str(); +} + std::string writeJson(const DepthOfFieldOptions& in) { std::ostringstream oss; oss << "{\n" diff --git a/libs/viewer/src/SimpleViewer.cpp b/libs/viewer/src/SimpleViewer.cpp index a0283faa43..9fe8932bf4 100644 --- a/libs/viewer/src/SimpleViewer.cpp +++ b/libs/viewer/src/SimpleViewer.cpp @@ -70,15 +70,15 @@ SimpleViewer::SimpleViewer(filament::Engine* engine, filament::Scene* scene, fil using namespace filament; LightManager::Builder(LightManager::Type::SUN) - .color(mSunlightColor) - .intensity(mSunlightIntensity) - .direction(normalize(mSunlightDirection)) + .color(mSettings.lighting.sunlightColor) + .intensity(mSettings.lighting.sunlightIntensity) + .direction(normalize(mSettings.lighting.sunlightDirection)) .castShadows(true) .sunAngularRadius(1.9) .sunHaloSize(10.0) .sunHaloFalloff(80.0) .build(*engine, mSunlight); - if (mEnableSunlight) { + if (mSettings.lighting.enableSunlight) { mScene->addEntity(mSunlight); } view->setAmbientOcclusionOptions({ .upsampling = View::QualityLevel::HIGH }); @@ -138,9 +138,9 @@ void SimpleViewer::setIndirectLight(filament::IndirectLight* ibl, if (ibl) { float3 d = filament::IndirectLight::getDirectionEstimate(sh3); float4 c = filament::IndirectLight::getColorEstimate(sh3, d); - mSunlightDirection = d; - mSunlightColor = c.rgb; - mSunlightIntensity = c[3] * ibl->getIntensity(); + mSettings.lighting.sunlightDirection = d; + mSettings.lighting.sunlightColor = c.rgb; + mSettings.lighting.sunlightIntensity = c[3] * ibl->getIntensity(); updateIndirectLight(); } } @@ -148,8 +148,8 @@ void SimpleViewer::setIndirectLight(filament::IndirectLight* ibl, void SimpleViewer::updateIndirectLight() { using namespace filament::math; if (mIndirectLight) { - mIndirectLight->setIntensity(mIblIntensity); - mIndirectLight->setRotation(mat3f::rotation(mIblRotation, float3{ 0, 1, 0 })); + mIndirectLight->setIntensity(mSettings.lighting.iblIntensity); + mIndirectLight->setRotation(mat3f::rotation(mSettings.lighting.iblRotation, float3{ 0, 1, 0 })); } } @@ -178,8 +178,8 @@ void SimpleViewer::renderUserInterface(float timeStepInSeconds, View* guiView, f mImGuiHelper = new ImGuiHelper(mEngine, guiView, ""); // TODO: this is not the best way to handle high DPI in ImGui, but it is fine when using the - // proggy font. Users need refresh their window when dragging between displas with different - // pixel ratios. + // proggy font. Users need to refresh their window when dragging between displays with + // different pixel ratios. ImGui::GetIO().FontGlobalScale = pixelRatio; ImGui::GetStyle().ScaleAllSizes(pixelRatio); } @@ -304,7 +304,7 @@ void SimpleViewer::updateUserInterface() { DebugRegistry& debug = mEngine->getDebugRegistry(); - if (ImGui::CollapsingHeader("View", ImGuiTreeNodeFlags_DefaultOpen)) { // TODO: do not commit + if (ImGui::CollapsingHeader("View")) { ImGui::Indent(); bool dither = mSettings.view.dithering == Dithering::TEMPORAL; @@ -363,14 +363,15 @@ void SimpleViewer::updateUserInterface() { ImGui::Unindent(); } + auto& light = mSettings.lighting; if (ImGui::CollapsingHeader("Light")) { ImGui::Indent(); - ImGui::SliderFloat("IBL intensity", &mIblIntensity, 0.0f, 100000.0f); - ImGui::SliderAngle("IBL rotation", &mIblRotation); - ImGui::SliderFloat("Sun intensity", &mSunlightIntensity, 50000.0, 150000.0f); - ImGuiExt::DirectionWidget("Sun direction", mSunlightDirection.v); - ImGui::Checkbox("Enable sunlight", &mEnableSunlight); - ImGui::Checkbox("Enable shadows", &mEnableShadows); + ImGui::SliderFloat("IBL intensity", &light.iblIntensity, 0.0f, 100000.0f); + ImGui::SliderAngle("IBL rotation", &light.iblRotation); + ImGui::SliderFloat("Sun intensity", &light.sunlightIntensity, 50000.0, 150000.0f); + ImGuiExt::DirectionWidget("Sun direction", light.sunlightDirection.v); + ImGui::Checkbox("Enable sunlight", &light.enableSunlight); + ImGui::Checkbox("Enable shadows", &light.enableShadows); bool enableVsm = mSettings.view.shadowType == ShadowType::VSM; ImGui::Checkbox("Enable VSM", &enableVsm); @@ -379,23 +380,26 @@ void SimpleViewer::updateUserInterface() { char label[32]; snprintf(label, 32, "%d", 1 << mVsmMsaaSamplesLog2); ImGui::SliderInt("VSM MSAA samples", &mVsmMsaaSamplesLog2, 0, 3, label); + light.shadowOptions.vsm.msaaSamples = static_cast(1u << mVsmMsaaSamplesLog2); int vsmAnisotropy = mSettings.view.vsmShadowOptions.anisotropy; snprintf(label, 32, "%d", 1 << vsmAnisotropy); ImGui::SliderInt("VSM anisotropy", &vsmAnisotropy, 0, 3, label); mSettings.view.vsmShadowOptions.anisotropy = vsmAnisotropy; - ImGui::SliderInt("Cascades", &mShadowCascades, 1, 4); + int shadowCascades = light.shadowOptions.shadowCascades; + ImGui::SliderInt("Cascades", &shadowCascades, 1, 4); ImGui::Checkbox("Debug cascades", debug.getPropertyAddress("d.shadowmap.visualize_cascades")); - ImGui::Checkbox("Enable contact shadows", &mEnableContactShadows); - ImGui::SliderFloat("Split pos 0", &mSplitPositions[0], 0.0f, 1.0f); - ImGui::SliderFloat("Split pos 1", &mSplitPositions[1], 0.0f, 1.0f); - ImGui::SliderFloat("Split pos 2", &mSplitPositions[2], 0.0f, 1.0f); + ImGui::Checkbox("Enable contact shadows", &light.shadowOptions.screenSpaceContactShadows); + ImGui::SliderFloat("Split pos 0", &light.shadowOptions.cascadeSplitPositions[0], 0.0f, 1.0f); + ImGui::SliderFloat("Split pos 1", &light.shadowOptions.cascadeSplitPositions[1], 0.0f, 1.0f); + ImGui::SliderFloat("Split pos 2", &light.shadowOptions.cascadeSplitPositions[2], 0.0f, 1.0f); ImGui::Unindent(); + light.shadowOptions.shadowCascades = shadowCascades; } - if (ImGui::CollapsingHeader("Fog", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::CollapsingHeader("Fog")) { ImGui::Indent(); ImGui::Checkbox("Enable fog", &mSettings.view.fog.enabled); ImGui::SliderFloat("Start", &mSettings.view.fog.distance, 0.0f, 100.0f); @@ -413,28 +417,21 @@ void SimpleViewer::updateUserInterface() { // so we can now push them into the Filament View. applySettings(mSettings.view, mView); - if (mEnableSunlight) { + if (light.enableSunlight) { mScene->addEntity(mSunlight); auto sun = lm.getInstance(mSunlight); - lm.setIntensity(sun, mSunlightIntensity); - lm.setDirection(sun, normalize(mSunlightDirection)); - lm.setColor(sun, mSunlightColor); - lm.setShadowCaster(sun, mEnableShadows); - auto options = lm.getShadowOptions(sun); - options.vsm.msaaSamples = static_cast(1u << mVsmMsaaSamplesLog2); - lm.setShadowOptions(sun, options); + lm.setIntensity(sun, light.sunlightIntensity); + lm.setDirection(sun, normalize(light.sunlightDirection)); + lm.setColor(sun, light.sunlightColor); + lm.setShadowCaster(sun, light.enableShadows); + lm.setShadowOptions(sun, light.shadowOptions); } else { mScene->remove(mSunlight); } - lm.forEachComponent([this, &lm](utils::Entity e, LightManager::Instance ci) { - auto options = lm.getShadowOptions(ci); - options.screenSpaceContactShadows = mEnableContactShadows; - options.shadowCascades = mShadowCascades; - options.vsm.msaaSamples = static_cast(1u << mVsmMsaaSamplesLog2); - std::copy_n(mSplitPositions.begin(), 3, options.cascadeSplitPositions); - lm.setShadowOptions(ci, options); - lm.setShadowCaster(ci, mEnableShadows); + lm.forEachComponent([this, &lm, &light](utils::Entity e, LightManager::Instance ci) { + lm.setShadowOptions(ci, light.shadowOptions); + lm.setShadowCaster(ci, light.enableShadows); }); if (mAsset != nullptr) { diff --git a/web/samples/remote.html b/web/samples/remote.html index b65a8ceec1..d9390231be 100644 --- a/web/samples/remote.html +++ b/web/samples/remote.html @@ -104,15 +104,13 @@ document.getElementById("copyButton").addEventListener("click", () => { dropbox.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation() }, false) }) -Filament.init([], () => { window.app = new App() }); +Filament.init([], () => window.app = new App() ); class App { constructor(canvas) { this.websocket = null; this.pollForServer = null; - this.widgetsDb = []; this.pendingFile = null; - this.fileType = "glb"; this.dropbox = document.getElementById("dropbox"); this.status = document.getElementById("status"); @@ -123,12 +121,8 @@ class App { const view = this.view = engine.createView(); const uiview = this.uiview = engine.createView(); - const L = 189 / 255; - const kBackgroundColor = [L, L, L, 1]; - this.swapChain = engine.createSwapChain(); this.renderer = engine.createRenderer(); - this.renderer.setClearOptions({clearColor: kBackgroundColor, clear: true}); this.camera = engine.createCamera(Filament.EntityManager.get().create()); this.serializer = new Filament.JsonSerializer(); this.previousSettingsJson = ""; @@ -138,9 +132,14 @@ class App { view.setPostProcessingEnabled(false); // For now, we initialize the "sidebar" such that it stretches across the entire viewport. - // In the future we might want to draw stuff in the central area. + // In the future we might want to draw stuff in the central 3D viewport. const kInitialSidebarWidth = this.canvas.clientWidth * window.devicePixelRatio; + // Clear the central 3D viewport to light gray, whicht is not visible by default. + const L = 189 / 255; + const kBackgroundColor = [L, L, L, 1]; + this.renderer.setClearOptions({clearColor: kBackgroundColor, clear: true}); + this.render = this.render.bind(this); this.simpleViewer = new Filament.SimpleViewer(engine, scene, view, kInitialSidebarWidth); @@ -192,7 +191,6 @@ class App { const is_glb = file.name.match(/\.(glb)$/i); const is_zip = file.name.match(/\.(zip)$/i); if (!is_glb && !is_zip) return; - if (is_zip) fileType = "zip"; const files = event.dataTransfer.files; ([...files]).forEach(upload); }, false); @@ -217,17 +215,14 @@ class App { this.status.style.backgroundColor = this.websocket ? "#45d48d" : "burlywood"; } - resetWidgetsDb() { - this.widgetsDb = []; - } - startSocket() { const ws = new WebSocket("ws://localhost:8082"); ws.addEventListener("open", () => { clearTimeout(this.pollForServer); - this.resetWidgetsDb(); + this.websocket = ws; this.updateDom(); + this.previousSettingsJson = ""; if (this.pendingFile) { this.uploadFile(this.pendingFile); this.pendingFile = null; @@ -237,7 +232,6 @@ class App { ws.addEventListener("close", (e) => { this.websocket = null; this.pollForServer = setTimeout(() => this.startSocket(), 3000); - this.resetWidgetsDb(); this.updateDom(); }); @@ -252,30 +246,42 @@ class App { return; } for (const command of commands) { - // TODO + // TODO: process incoming messages here. + // Currently we do not send messages from the app on the device to the web client. } }); - - this.websocket = ws; } render() { - const dt = 1.0 / 60.0; + const connection = this.websocket; - // We only process only a single mouse button event here because we want ImGui to detect - // a touch event even when a down-up pair occurs between consecutive frames. + // Process only a single mouse button event to ensure that ImGui detects a touch event + // even when a down-up pair occurs between consecutive frames. let mouseButton = this.mouseButton; if (this.mouseButtonEvents.length > 0) { this.mouseButton = this.mouseButtonEvents.shift(); } + const mouseWheel = this.mouseWheelY; + this.mouseWheelY = 0; - this.simpleViewer.renderUserInterface(dt, this.uiview, window.devicePixelRatio, - this.mouseX, this.mouseY, this.mouseButton, this.mouseWheelY); + // If there's no connection, let Filament clear the canvas, but do not render the UI view. + if (!connection) { + this.renderer.beginFrame(this.swapChain); + this.renderer.renderView(this.view); + this.renderer.endFrame(); + this.engine.execute(); + window.requestAnimationFrame(this.render); + return; + } + // Draw the UI and potentially mutate the settings object. + const deltaTime = 1.0 / 60.0; + this.simpleViewer.renderUserInterface(deltaTime, this.uiview, window.devicePixelRatio, + this.mouseX, this.mouseY, this.mouseButton, mouseWheel); + + // Check if the user has changed any settings. const settingsJson = this.serializer.writeJson(this.simpleViewer.getSettings()).slice(); - if (this.previousSettingsJson != settingsJson) { - const connection = this.websocket; if (connection && connection.readyState == connection.OPEN) { connection.send("setttings.json"); connection.send(settingsJson); @@ -283,8 +289,7 @@ class App { this.previousSettingsJson = settingsJson; } - this.mouseWheelY = 0; - + // Use Filament to render the 3D viewport and the UI. this.renderer.beginFrame(this.swapChain); this.renderer.renderView(this.view); this.renderer.renderView(this.uiview);