Files
filament/filament/src/details/View.cpp
T
Sungun Park 142b73d9d7 Add OpenGL extension for multiview (#7569)
* Add OpenGL extension for multiview

This extension is going to be used for multiview implementation in
OpenGL.

Now the API isStereoSupported takes a stereo type as a parameter.
2024-02-15 08:21:00 -08:00

1187 lines
50 KiB
C++

/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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
*
* http://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.
*/
#include "details/View.h"
#include "Culler.h"
#include "Froxelizer.h"
#include "RenderPrimitive.h"
#include "ResourceAllocator.h"
#include "ShadowMapManager.h"
#include "details/Engine.h"
#include "details/IndirectLight.h"
#include "details/RenderTarget.h"
#include "details/Renderer.h"
#include "details/Scene.h"
#include "details/Skybox.h"
#include <filament/Exposure.h>
#include <filament/TextureSampler.h>
#include <filament/View.h>
#include <private/filament/UibStructs.h>
#include <utils/Profiler.h>
#include <utils/Slice.h>
#include <utils/Systrace.h>
#include <utils/debug.h>
#include <utils/Zip2Iterator.h>
#include <math/scalar.h>
#include <math/fast.h>
#include <array>
#include <memory>
using namespace utils;
namespace filament {
using namespace backend;
using namespace math;
static constexpr float PID_CONTROLLER_Ki = 0.002f;
static constexpr float PID_CONTROLLER_Kd = 0.0f;
FView::FView(FEngine& engine)
: mFroxelizer(engine),
mFogEntity(engine.getEntityManager().create()),
mIsStereoSupported(engine.getDriverApi().isStereoSupported(engine.getConfig().stereoscopicType)),
mPerViewUniforms(engine) {
DriverApi& driver = engine.getDriverApi();
FDebugRegistry& debugRegistry = engine.getDebugRegistry();
debugRegistry.registerProperty("d.view.camera_at_origin",
&engine.debug.view.camera_at_origin);
// The integral term is used to fight back the dead-band below, we limit how much it can act.
mPidController.setIntegralLimits(-100.0f, 100.0f);
// Dead-band, 1% for scaling down, 5% for scaling up. This stabilizes all the jitters.
mPidController.setOutputDeadBand(-0.01f, 0.05f);
#ifndef NDEBUG
debugRegistry.registerDataSource("d.view.frame_info",
[this]() -> DebugRegistry::DataSource {
assert_invariant(!mDebugFrameHistory);
mDebugFrameHistory = std::make_unique<std::array<DebugRegistry::FrameHistory, 5*60>>();
return { mDebugFrameHistory->data(), mDebugFrameHistory->size() };
});
debugRegistry.registerProperty("d.view.pid.kp", &engine.debug.view.pid.kp);
debugRegistry.registerProperty("d.view.pid.ki", &engine.debug.view.pid.ki);
debugRegistry.registerProperty("d.view.pid.kd", &engine.debug.view.pid.kd);
// default parameters for debugging UI
engine.debug.view.pid.kp = 1.0f - std::exp(-1.0f / 8.0f);
engine.debug.view.pid.ki = PID_CONTROLLER_Ki;
engine.debug.view.pid.kd = PID_CONTROLLER_Kd;
mPidController.setParallelGains(
engine.debug.view.pid.kp, engine.debug.view.pid.ki, engine.debug.view.pid.kd);
#endif
// allocate UBOs
mLightUbh = driver.createBufferObject(CONFIG_MAX_LIGHT_COUNT * sizeof(LightsUib),
BufferObjectBinding::UNIFORM, BufferUsage::DYNAMIC);
mIsDynamicResolutionSupported = driver.isFrameTimeSupported();
mDefaultColorGrading = mColorGrading = engine.getDefaultColorGrading();
}
FView::~FView() noexcept = default;
void FView::terminate(FEngine& engine) {
// Here we would cleanly free resources we've allocated, or we own (currently none).
while (mActivePickingQueriesList) {
FPickingQuery* const pQuery = mActivePickingQueriesList;
mActivePickingQueriesList = pQuery->next;
pQuery->callback(pQuery->result, pQuery);
FPickingQuery::put(pQuery);
}
DriverApi& driver = engine.getDriverApi();
driver.destroyBufferObject(mLightUbh);
driver.destroyBufferObject(mRenderableUbh);
drainFrameHistory(engine);
ShadowMapManager::terminate(engine, mShadowMapManager);
mPerViewUniforms.terminate(driver);
mFroxelizer.terminate(driver);
engine.getEntityManager().destroy(mFogEntity);
}
void FView::setViewport(filament::Viewport const& viewport) noexcept {
// catch the cases were user had an underflow and didn't catch it.
assert((int32_t)viewport.width > 0);
assert((int32_t)viewport.height > 0);
mViewport = viewport;
}
void FView::setDynamicResolutionOptions(DynamicResolutionOptions const& options) noexcept {
DynamicResolutionOptions& dynamicResolution = mDynamicResolution;
dynamicResolution = options;
// only enable if dynamic resolution is supported
dynamicResolution.enabled = dynamicResolution.enabled && mIsDynamicResolutionSupported;
if (dynamicResolution.enabled) {
// if enabled, sanitize the parameters
// minScale cannot be 0 or negative
dynamicResolution.minScale = max(dynamicResolution.minScale, float2(1.0f / 1024.0f));
// maxScale cannot be < minScale
dynamicResolution.maxScale = max(dynamicResolution.maxScale, dynamicResolution.minScale);
// clamp maxScale to 2x because we're doing bilinear filtering, so super-sampling
// is not useful above that.
dynamicResolution.maxScale = min(dynamicResolution.maxScale, float2(2.0f));
dynamicResolution.sharpness = clamp(dynamicResolution.sharpness, 0.0f, 2.0f);
}
}
void FView::setDynamicLightingOptions(float zLightNear, float zLightFar) noexcept {
mFroxelizer.setOptions(zLightNear, zLightFar);
}
float2 FView::updateScale(FEngine& engine,
FrameInfo const& info,
Renderer::FrameRateOptions const& frameRateOptions,
Renderer::DisplayInfo const& displayInfo) noexcept {
DynamicResolutionOptions const& options = mDynamicResolution;
if (options.enabled) {
if (!UTILS_UNLIKELY(info.valid)) {
// always clamp to the min/max scale range
mScale = clamp(1.0f, options.minScale, options.maxScale);
return mScale;
}
#ifndef NDEBUG
const float Kp = engine.debug.view.pid.kp;
const float Ki = engine.debug.view.pid.ki;
const float Kd = engine.debug.view.pid.kd;
#else
const float Kp = (1.0f - std::exp(-frameRateOptions.scaleRate));
const float Ki = PID_CONTROLLER_Ki;
const float Kd = PID_CONTROLLER_Kd;
#endif
mPidController.setParallelGains(Kp, Ki, Kd);
// all values in ms below
using std::chrono::duration;
const float dt = 1.0f; // we don't really need dt here, setting it to 1, means our parameters are in "frames"
const float target = (1000.0f * float(frameRateOptions.interval)) / displayInfo.refreshRate;
const float targetWithHeadroom = target * (1.0f - frameRateOptions.headRoomRatio);
float const measured = duration<float, std::milli>{ info.denoisedFrameTime }.count();
float const out = mPidController.update(measured / targetWithHeadroom, 1.0f, dt);
// maps pid command to a scale (absolute or relative, see below)
const float command = out < 0.0f ? (1.0f / (1.0f - out)) : (1.0f + out);
/*
* There is two ways we can control the scale factor, either by having the PID controller
* output a new scale factor directly (like a "position" control), or having it evaluate
* a relative scale factor (like a "velocity" control).
* More experimentation is needed to figure out which works better in more cases.
*/
// direct scaling ("position" control)
//const float scale = command;
// relative scaling ("velocity" control)
const float scale = mScale.x * mScale.y * command;
const float w = float(mViewport.width);
const float h = float(mViewport.height);
if (scale < 1.0f && !options.homogeneousScaling) {
// figure out the major and minor axis
const float major = std::max(w, h);
const float minor = std::min(w, h);
// the major axis is scaled down first, down to the minor axis
const float maxMajorScale = minor / major;
const float majorScale = std::max(scale, maxMajorScale);
// then the minor axis is scaled down to the original aspect-ratio
const float minorScale = std::max(scale / majorScale, majorScale * maxMajorScale);
// if we have some scaling capacity left, scale homogeneously
const float homogeneousScale = scale / (majorScale * minorScale);
// finally, write the scale factors
float& majorRef = w > h ? mScale.x : mScale.y;
float& minorRef = w > h ? mScale.y : mScale.x;
majorRef = std::sqrt(homogeneousScale) * majorScale;
minorRef = std::sqrt(homogeneousScale) * minorScale;
} else {
// when scaling up, we're always using homogeneous scaling.
mScale = std::sqrt(scale);
}
// always clamp to the min/max scale range
const auto s = mScale;
mScale = clamp(s, options.minScale, options.maxScale);
// disable the integration term when we're outside the controllable range
// (i.e. we clamped). This help not to have to wait too long for the Integral term
// to kick in after a clamping event.
mPidController.setIntegralInhibitionEnabled(mScale != s);
} else {
mScale = 1.0f;
}
#ifndef NDEBUG
// only for debugging...
if (mDebugFrameHistory) {
using namespace std::chrono;
using duration_ms = duration<float, std::milli>;
const float target = (1000.0f * float(frameRateOptions.interval)) / displayInfo.refreshRate;
const float targetWithHeadroom = target * (1.0f - frameRateOptions.headRoomRatio);
std::move(mDebugFrameHistory->begin() + 1,
mDebugFrameHistory->end(), mDebugFrameHistory->begin());
mDebugFrameHistory->back() = {
.target = target,
.targetWithHeadroom = targetWithHeadroom,
.frameTime = duration_cast<duration_ms>(info.frameTime).count(),
.frameTimeDenoised = duration_cast<duration_ms>(info.denoisedFrameTime).count(),
.scale = mScale.x * mScale.y,
.pid_e = mPidController.getError(),
.pid_i = mPidController.getIntegral(),
.pid_d = mPidController.getDerivative()
};
}
#endif
return mScale;
}
void FView::setVisibleLayers(uint8_t select, uint8_t values) noexcept {
mVisibleLayers = (mVisibleLayers & ~select) | (values & select);
}
bool FView::isSkyboxVisible() const noexcept {
FSkybox const* skybox = mScene ? mScene->getSkybox() : nullptr;
return skybox != nullptr && (skybox->getLayerMask() & mVisibleLayers);
}
void FView::prepareShadowing(FEngine& engine, FScene::RenderableSoa& renderableData,
FScene::LightSoa const& lightData, CameraInfo const& cameraInfo) noexcept {
SYSTRACE_CALL();
mHasShadowing = false;
mNeedsShadowMap = false;
if (!mShadowingEnabled) {
return;
}
auto& lcm = engine.getLightManager();
ShadowMapManager::Builder builder;
// dominant directional light is always as index 0
FLightManager::Instance const directionalLight = lightData.elementAt<FScene::LIGHT_INSTANCE>(0);
const bool hasDirectionalShadows = directionalLight && lcm.isShadowCaster(directionalLight);
if (UTILS_UNLIKELY(hasDirectionalShadows)) {
const auto& shadowOptions = lcm.getShadowOptions(directionalLight);
assert_invariant(shadowOptions.shadowCascades >= 1 &&
shadowOptions.shadowCascades <= CONFIG_MAX_SHADOW_CASCADES);
builder.directionalShadowMap(0, &shadowOptions);
}
// Find all shadow-casting spotlights.
size_t shadowMapCount = CONFIG_MAX_SHADOW_CASCADES;
// We allow a max of CONFIG_MAX_SHADOWMAPS point/spotlight shadows. Any additional
// shadow-casting spotlights are ignored.
// Note that pointlight shadows cost 6 shadowmaps, reducing the total count.
for (size_t l = FScene::DIRECTIONAL_LIGHTS_COUNT; l < lightData.size(); l++) {
// when we get here all the lights should be visible
assert_invariant(lightData.elementAt<FScene::VISIBILITY>(l));
FLightManager::Instance const li = lightData.elementAt<FScene::LIGHT_INSTANCE>(l);
if (UTILS_LIKELY(!li)) {
continue; // invalid instance
}
if (UTILS_LIKELY(!lcm.isShadowCaster(li))) {
// Because we early exit here, we need to make sure we mark the light as non-casting.
// See `ShadowMapManager::updateSpotShadowMaps` for const_cast<> justification.
const_cast<FScene::ShadowInfo&>(
lightData.elementAt<FScene::SHADOW_INFO>(l)).castsShadows = false;
continue; // doesn't cast shadows
}
const bool spotLight = lcm.isSpotLight(li);
const size_t shadowMapCountNeeded = spotLight ? 1 : 6;
if (shadowMapCount + shadowMapCountNeeded <= CONFIG_MAX_SHADOWMAPS) {
shadowMapCount += shadowMapCountNeeded;
const auto& shadowOptions = lcm.getShadowOptions(li);
builder.shadowMap(l, spotLight, &shadowOptions);
}
if (shadowMapCount >= CONFIG_MAX_SHADOWMAPS) {
break; // we ran out of spotlight shadow casting
}
}
if (builder.hasShadowMaps()) {
ShadowMapManager::createIfNeeded(engine, mShadowMapManager);
auto shadowTechnique = mShadowMapManager->update(builder, engine, *this,
cameraInfo, renderableData, lightData);
mHasShadowing = any(shadowTechnique);
mNeedsShadowMap = any(shadowTechnique & ShadowMapManager::ShadowTechnique::SHADOW_MAP);
}
}
void FView::prepareLighting(FEngine& engine, CameraInfo const& cameraInfo) noexcept {
SYSTRACE_CALL();
SYSTRACE_CONTEXT();
FScene* const scene = mScene;
auto const& lightData = scene->getLightData();
/*
* Dynamic lights
*/
if (hasDynamicLighting()) {
scene->prepareDynamicLights(cameraInfo, mLightUbh);
}
// here the array of visible lights has been shrunk to CONFIG_MAX_LIGHT_COUNT
SYSTRACE_VALUE32("visibleLights", lightData.size() - FScene::DIRECTIONAL_LIGHTS_COUNT);
/*
* Exposure
*/
const float exposure = Exposure::exposure(cameraInfo.ev100);
mPerViewUniforms.prepareExposure(cameraInfo.ev100);
/*
* Indirect light (IBL)
*/
// If the scene does not have an IBL, use the black 1x1 IBL and honor the fallback intensity
// associated with the skybox.
float intensity;
FIndirectLight const* ibl = scene->getIndirectLight();
if (UTILS_LIKELY(ibl)) {
intensity = ibl->getIntensity();
} else {
ibl = engine.getDefaultIndirectLight();
FSkybox const* const skybox = scene->getSkybox();
intensity = skybox ? skybox->getIntensity() : FIndirectLight::DEFAULT_INTENSITY;
}
mPerViewUniforms.prepareAmbientLight(engine, *ibl, intensity, exposure);
/*
* Directional light (always at index 0)
*/
FLightManager::Instance const directionalLight = lightData.elementAt<FScene::LIGHT_INSTANCE>(0);
const float3 sceneSpaceDirection = lightData.elementAt<FScene::DIRECTION>(0); // guaranteed normalized
mPerViewUniforms.prepareDirectionalLight(engine, exposure, sceneSpaceDirection, directionalLight);
}
CameraInfo FView::computeCameraInfo(FEngine& engine) const noexcept {
FScene const* const scene = getScene();
/*
* We apply a "world origin" to "everything" in order to implement the IBL rotation.
* The "world origin" is also used to keep the origin close to the camera position to
* improve fp precision in the shader for large scenes.
*/
double3 translation;
mat3 rotation;
/*
* Calculate all camera parameters needed to render this View for this frame.
*/
FCamera const* const camera = mViewingCamera ? mViewingCamera : mCullingCamera;
if (engine.debug.view.camera_at_origin) {
// this moves the camera to the origin, effectively doing all shader computations in
// view-space, which improves floating point precision in the shader by staying around
// zero, where fp precision is highest. This also ensures that when the camera is placed
// very far from the origin, objects are still rendered and lit properly.
translation = -camera->getPosition();
}
FIndirectLight const* const ibl = scene->getIndirectLight();
if (ibl) {
// the IBL transformation must be a rigid transform
rotation = mat3{ transpose(scene->getIndirectLight()->getRotation()) };
// it is important to orthogonalize the matrix when converting it to doubles, because
// as float, it only has about a 1e-8 precision on the size of the basis vectors
rotation = orthogonalize(rotation);
}
return { *camera, mat4{ rotation } * mat4::translation(translation) };
}
void FView::prepare(FEngine& engine, DriverApi& driver, RootArenaScope& rootArenaScope,
filament::Viewport viewport, CameraInfo cameraInfo,
float4 const& userTime, bool needsAlphaChannel) noexcept {
SYSTRACE_CALL();
SYSTRACE_CONTEXT();
JobSystem& js = engine.getJobSystem();
/*
* Prepare the scene -- this is where we gather all the objects added to the scene,
* and in particular their world-space AABB.
*/
auto getFrustum = [this, &cameraInfo]() -> Frustum {
if (UTILS_LIKELY(mViewingCamera == nullptr)) {
// In the common case when we don't have a viewing camera, cameraInfo.view is
// already the culling view matrix
return Frustum{ mat4f{ highPrecisionMultiply(cameraInfo.cullingProjection, cameraInfo.view) }};
} else {
// Otherwise, we need to recalculate it from the culling camera.
// Note: it is correct to always do the math from mCullingCamera, but it hides the
// intent of the code, which is that we should only depend on CameraInfo here.
// This is an extremely uncommon case.
const mat4 projection = mCullingCamera->getCullingProjectionMatrix();
const mat4 view = inverse(cameraInfo.worldTransform * mCullingCamera->getModelMatrix());
return Frustum{ mat4f{ projection * view }};
}
};
const Frustum cullingFrustum = getFrustum();
FScene* const scene = getScene();
/*
* Gather all information needed to render this scene. Apply the world origin to all
* objects in the scene.
*/
scene->prepare(js, rootArenaScope,
cameraInfo.worldTransform,
hasVSM());
/*
* Light culling: runs in parallel with Renderable culling (below)
*/
JobSystem::Job* froxelizeLightsJob = nullptr;
JobSystem::Job* prepareVisibleLightsJob = nullptr;
size_t const lightCount = scene->getLightData().size();
if (lightCount > FScene::DIRECTIONAL_LIGHTS_COUNT) {
// create and start the prepareVisibleLights job
// note: this job updates LightData (non const)
// allocate a scratch buffer for distances outside the job below, so we don't need
// to use a locked allocator; the downside is that we need to account for the worst case.
size_t const positionalLightCount = lightCount - FScene::DIRECTIONAL_LIGHTS_COUNT;
float* const distances = rootArenaScope.allocate<float>(
(positionalLightCount + 3u) & ~3u, CACHELINE_SIZE);
prepareVisibleLightsJob = js.runAndRetain(js.createJob(nullptr,
[&engine, distances, positionalLightCount, &viewMatrix = cameraInfo.view, &cullingFrustum,
&lightData = scene->getLightData()]
(JobSystem&, JobSystem::Job*) {
FView::prepareVisibleLights(engine.getLightManager(),
{ distances, distances + positionalLightCount },
viewMatrix, cullingFrustum, lightData);
}));
}
// this is used later (in Renderer.cpp) to wait for froxelization to finishes
setFroxelizerSync(froxelizeLightsJob);
Range merged;
FScene::RenderableSoa& renderableData = scene->getRenderableData();
{ // all the operations in this scope must happen sequentially
Slice<Culler::result_type> cullingMask = renderableData.slice<FScene::VISIBLE_MASK>();
std::uninitialized_fill(cullingMask.begin(), cullingMask.end(), 0);
/*
* Culling: as soon as possible we perform our camera-culling
* (this will set the VISIBLE_RENDERABLE bit)
*/
prepareVisibleRenderables(js, cullingFrustum, renderableData);
/*
* Shadowing: compute the shadow camera and cull shadow casters
* (this will set the VISIBLE_DIR_SHADOW_CASTER bit and VISIBLE_SPOT_SHADOW_CASTER bits)
*/
// prepareShadowing relies on prepareVisibleLights().
if (prepareVisibleLightsJob) {
js.waitAndRelease(prepareVisibleLightsJob);
}
// lightData is const from this point on (can only happen after prepareVisibleLightsJob)
auto const& lightData = scene->getLightData();
// now we know if we have dynamic lighting (i.e.: dynamic lights are visible)
mHasDynamicLighting = lightData.size() > FScene::DIRECTIONAL_LIGHTS_COUNT;
// we also know if we have a directional light
FLightManager::Instance const directionalLight =
lightData.elementAt<FScene::LIGHT_INSTANCE>(0);
mHasDirectionalLight = directionalLight.isValid();
// As soon as prepareVisibleLight finishes, we can kick-off the froxelization
if (hasDynamicLighting()) {
auto& froxelizer = mFroxelizer;
if (froxelizer.prepare(driver, rootArenaScope, viewport,
cameraInfo.projection, cameraInfo.zn, cameraInfo.zf)) {
// TODO: might be more consistent to do this in prepareLighting(), but it's not
// strictly necessary
mPerViewUniforms.prepareDynamicLights(mFroxelizer);
}
// We need to pass viewMatrix by value here because it extends the scope of this
// function.
std::function<void(JobSystem&, JobSystem::Job*)> froxelizerWork =
[&froxelizer = mFroxelizer, &engine, viewMatrix = cameraInfo.view, &lightData]
(JobSystem&, JobSystem::Job*) {
froxelizer.froxelizeLights(engine, viewMatrix, lightData);
};
froxelizeLightsJob = js.runAndRetain(js.createJob(nullptr, std::move(froxelizerWork)));
}
setFroxelizerSync(froxelizeLightsJob);
prepareShadowing(engine, renderableData, lightData, cameraInfo);
/*
* Partition the SoA so that renderables are partitioned w.r.t their visibility into the
* following groups:
*
* 1. visible (main camera) renderables
* 2. visible (main camera) renderables and directional shadow casters
* 3. directional shadow casters only
* 4. potential punctual light shadow casters only
* 5. definitely invisible renderables
*
* Note that the first three groups are partitioned based only on the lowest two bits of the
* VISIBLE_MASK (VISIBLE_RENDERABLE and VISIBLE_DIR_SHADOW_CASTER), and thus can also
* contain punctual light shadow casters as well. The fourth group contains *only* punctual
* shadow casters.
*
* This operation is somewhat heavy as it sorts the whole SoA. We use std::partition instead
* of sort(), which gives us O(4.N) instead of O(N.log(N)) application of swap().
*/
// TODO: we need to compare performance of doing this partitioning vs not doing it.
// and rely on checking visibility in the loops
SYSTRACE_NAME_BEGIN("Partitioning");
// calculate the sorting key for all elements, based on their visibility
uint8_t const* layers = renderableData.data<FScene::LAYERS>();
auto const* visibility = renderableData.data<FScene::VISIBILITY_STATE>();
computeVisibilityMasks(getVisibleLayers(), layers, visibility, cullingMask.begin(),
renderableData.size());
auto const beginRenderables = renderableData.begin();
auto beginDirCasters = partition(beginRenderables, renderableData.end(),
VISIBLE_RENDERABLE | VISIBLE_DIR_SHADOW_RENDERABLE,
VISIBLE_RENDERABLE);
auto beginDirCastersOnly = partition(beginDirCasters, renderableData.end(),
VISIBLE_RENDERABLE | VISIBLE_DIR_SHADOW_RENDERABLE,
VISIBLE_RENDERABLE | VISIBLE_DIR_SHADOW_RENDERABLE);
auto endDirCastersOnly = partition(beginDirCastersOnly, renderableData.end(),
VISIBLE_RENDERABLE | VISIBLE_DIR_SHADOW_RENDERABLE,
VISIBLE_DIR_SHADOW_RENDERABLE);
auto endPotentialSpotCastersOnly = partition(endDirCastersOnly, renderableData.end(),
VISIBLE_DYN_SHADOW_RENDERABLE,
VISIBLE_DYN_SHADOW_RENDERABLE);
// convert to indices
mVisibleRenderables = { 0, uint32_t(beginDirCastersOnly - beginRenderables) };
mVisibleDirectionalShadowCasters = {
uint32_t(beginDirCasters - beginRenderables),
uint32_t(endDirCastersOnly - beginRenderables)};
merged = { 0, uint32_t(endPotentialSpotCastersOnly - beginRenderables) };
if (!needsShadowMap() || !mShadowMapManager->hasSpotShadows()) {
// we know we don't have spot shadows, we can reduce the range to not even include
// the potential spot casters
merged = { 0, uint32_t(endDirCastersOnly - beginRenderables) };
}
mSpotLightShadowCasters = merged;
SYSTRACE_NAME_END();
// TODO: when any spotlight is used, `merged` ends-up being the whole list. However,
// some of the items will end-up not being visible by any light. Can we do better?
// e.g. could we deffer some of the prepareVisibleRenderables() to later?
scene->prepareVisibleRenderables(merged);
// update those UBOs
const size_t size = merged.size() * sizeof(PerRenderableData);
if (size) {
if (mRenderableUBOSize < size) {
// allocate 1/3 extra, with a minimum of 16 objects
const size_t count = std::max(size_t(16u), (4u * merged.size() + 2u) / 3u);
mRenderableUBOSize = uint32_t(count * sizeof(PerRenderableData));
driver.destroyBufferObject(mRenderableUbh);
mRenderableUbh = driver.createBufferObject(mRenderableUBOSize + sizeof(PerRenderableUib),
BufferObjectBinding::UNIFORM, BufferUsage::DYNAMIC);
} else {
// TODO: should we shrink the underlying UBO at some point?
}
assert_invariant(mRenderableUbh);
scene->updateUBOs(merged, mRenderableUbh);
}
}
/*
* Prepare lighting -- this is where we update the lights UBOs, set up the IBL,
* set up the froxelization parameters.
* Relies on FScene::prepare() and prepareVisibleLights()
*/
prepareLighting(engine, cameraInfo);
/*
* Update driver state
*/
auto const& tcm = engine.getTransformManager();
auto const fogTransform = tcm.getWorldTransformAccurate(tcm.getInstance(mFogEntity));
mPerViewUniforms.prepareTime(engine, userTime);
mPerViewUniforms.prepareFog(engine, cameraInfo, fogTransform, mFogOptions,
scene->getIndirectLight());
mPerViewUniforms.prepareTemporalNoise(engine, mTemporalAntiAliasingOptions);
mPerViewUniforms.prepareBlending(needsAlphaChannel);
mPerViewUniforms.prepareMaterialGlobals(mMaterialGlobals);
}
void FView::bindPerViewUniformsAndSamplers(FEngine::DriverApi& driver) const noexcept {
mPerViewUniforms.bind(driver);
if (UTILS_UNLIKELY(driver.getFeatureLevel() == backend::FeatureLevel::FEATURE_LEVEL_0)) {
return;
}
driver.bindUniformBuffer(+UniformBindingPoints::LIGHTS,
mLightUbh);
if (needsShadowMap()) {
assert_invariant(mShadowMapManager->getShadowUniformsHandle());
driver.bindUniformBuffer(+UniformBindingPoints::SHADOW,
mShadowMapManager->getShadowUniformsHandle());
}
driver.bindUniformBuffer(+UniformBindingPoints::FROXEL_RECORDS,
mFroxelizer.getRecordBuffer());
driver.bindUniformBuffer(+UniformBindingPoints::FROXELS,
mFroxelizer.getFroxelBuffer());
}
void FView::computeVisibilityMasks(
uint8_t visibleLayers,
uint8_t const* UTILS_RESTRICT layers,
FRenderableManager::Visibility const* UTILS_RESTRICT visibility,
Culler::result_type* UTILS_RESTRICT visibleMask, size_t count) {
// __restrict__ seems to only be taken into account as function parameters. This is very
// important here, otherwise, this loop doesn't get vectorized.
// This is vectorized 16x.
count = (count + 0xFu) & ~0xFu; // capacity guaranteed to be multiple of 16
for (size_t i = 0; i < count; ++i) {
const Culler::result_type mask = visibleMask[i];
const FRenderableManager::Visibility v = visibility[i];
const bool inVisibleLayer = layers[i] & visibleLayers;
const bool visibleRenderable = inVisibleLayer &&
(!v.culling || (mask & VISIBLE_RENDERABLE));
const bool visibleDirectionalShadowRenderable = (v.castShadows && inVisibleLayer) &&
(!v.culling || (mask & VISIBLE_DIR_SHADOW_RENDERABLE));
const bool potentialSpotShadowRenderable = v.castShadows && inVisibleLayer;
using Type = Culler::result_type;
visibleMask[i] =
Type(visibleRenderable << VISIBLE_RENDERABLE_BIT) |
Type(visibleDirectionalShadowRenderable << VISIBLE_DIR_SHADOW_RENDERABLE_BIT) |
Type(potentialSpotShadowRenderable << VISIBLE_DYN_SHADOW_RENDERABLE_BIT);
}
}
UTILS_NOINLINE
/* static */ FScene::RenderableSoa::iterator FView::partition(
FScene::RenderableSoa::iterator begin,
FScene::RenderableSoa::iterator end,
Culler::result_type mask, Culler::result_type value) noexcept {
return std::partition(begin, end, [mask, value](auto it) {
// Mask VISIBLE_MASK to ignore higher bits related to spot shadows. We only partition based
// on renderable and directional shadow visibility.
return (it.template get<FScene::VISIBLE_MASK>() & mask) == value;
});
}
void FView::prepareUpscaler(float2 scale,
TemporalAntiAliasingOptions const& taaOptions,
DynamicResolutionOptions const& dsrOptions) const noexcept {
SYSTRACE_CALL();
float bias = 0.0f;
float2 derivativesScale{ 1.0f };
if (dsrOptions.enabled && dsrOptions.quality >= QualityLevel::HIGH) {
bias = std::log2(std::min(scale.x, scale.y));
}
if (taaOptions.enabled) {
bias += taaOptions.lodBias;
if (taaOptions.upscaling) {
derivativesScale = 0.5f;
}
}
mPerViewUniforms.prepareLodBias(bias, derivativesScale);
}
void FView::prepareCamera(FEngine& engine, const CameraInfo& cameraInfo) const noexcept {
SYSTRACE_CALL();
mPerViewUniforms.prepareCamera(engine, cameraInfo);
}
void FView::prepareViewport(
const filament::Viewport& physicalViewport,
const filament::Viewport& logicalViewport) const noexcept {
SYSTRACE_CALL();
// TODO: we should pass viewport.{left|bottom} to the backend, so it can offset the
// scissor properly.
mPerViewUniforms.prepareViewport(physicalViewport, logicalViewport);
}
void FView::prepareSSAO(Handle<HwTexture> ssao) const noexcept {
mPerViewUniforms.prepareSSAO(ssao, mAmbientOcclusionOptions);
}
void FView::prepareSSR(Handle<HwTexture> ssr,
bool disableSSR,
float refractionLodOffset,
ScreenSpaceReflectionsOptions const& ssrOptions) const noexcept {
mPerViewUniforms.prepareSSR(ssr, disableSSR, refractionLodOffset, ssrOptions);
}
void FView::prepareStructure(Handle<HwTexture> structure) const noexcept {
// sampler must be NEAREST
mPerViewUniforms.prepareStructure(structure);
}
void FView::prepareShadow(Handle<HwTexture> texture) const noexcept {
// when needsShadowMap() is not set, this method only just sets a dummy texture
// in the needed samplers (in that case `texture` is actually a dummy texture).
ShadowMapManager::ShadowMappingUniforms uniforms;
if (needsShadowMap()) {
uniforms = mShadowMapManager->getShadowMappingUniforms();
}
switch (mShadowType) {
case filament::ShadowType::PCF:
mPerViewUniforms.prepareShadowPCF(texture, uniforms);
break;
case filament::ShadowType::VSM:
mPerViewUniforms.prepareShadowVSM(texture, uniforms, mVsmShadowOptions);
break;
case filament::ShadowType::DPCF:
mPerViewUniforms.prepareShadowDPCF(texture, uniforms, mSoftShadowOptions);
break;
case filament::ShadowType::PCSS:
mPerViewUniforms.prepareShadowPCSS(texture, uniforms, mSoftShadowOptions);
break;
case filament::ShadowType::PCFd:
mPerViewUniforms.prepareShadowPCFDebug(texture, uniforms);
break;
}
}
void FView::prepareShadowMapping(bool highPrecision) const noexcept {
mPerViewUniforms.prepareShadowMapping(highPrecision);
}
void FView::cleanupRenderPasses() const noexcept {
mPerViewUniforms.unbindSamplers();
}
void FView::commitUniforms(DriverApi& driver) const noexcept {
mPerViewUniforms.commit(driver);
}
void FView::commitFroxels(DriverApi& driverApi) const noexcept {
if (mHasDynamicLighting) {
mFroxelizer.commit(driverApi);
}
}
UTILS_NOINLINE
void FView::prepareVisibleRenderables(JobSystem& js,
Frustum const& frustum, FScene::RenderableSoa& renderableData) const noexcept {
SYSTRACE_CALL();
if (UTILS_LIKELY(isFrustumCullingEnabled())) {
FView::cullRenderables(js, renderableData, frustum, VISIBLE_RENDERABLE_BIT);
} else {
std::uninitialized_fill(renderableData.begin<FScene::VISIBLE_MASK>(),
renderableData.end<FScene::VISIBLE_MASK>(), VISIBLE_RENDERABLE);
}
}
void FView::cullRenderables(JobSystem&,
FScene::RenderableSoa& renderableData, Frustum const& frustum, size_t bit) noexcept {
SYSTRACE_CALL();
float3 const* worldAABBCenter = renderableData.data<FScene::WORLD_AABB_CENTER>();
float3 const* worldAABBExtent = renderableData.data<FScene::WORLD_AABB_EXTENT>();
FScene::VisibleMaskType* visibleArray = renderableData.data<FScene::VISIBLE_MASK>();
// culling job (this runs on multiple threads)
auto functor = [&frustum, worldAABBCenter, worldAABBExtent, visibleArray, bit]
(uint32_t index, uint32_t c) {
Culler::intersects(
visibleArray + index,
frustum,
worldAABBCenter + index,
worldAABBExtent + index, c, bit);
};
// Note: we can't use jobs::parallel_for() here because Culler::intersects() must process
// multiples of eight primitives.
// Moreover, even with a large number of primitives, the overhead of the JobSystem is too
// large compared to the run time of Culler::intersects, e.g.: ~100us for 4000 primitives
// on Pixel4.
functor(0, renderableData.size());
}
void FView::prepareVisibleLights(FLightManager const& lcm,
utils::Slice<float> scratch,
mat4f const& viewMatrix, Frustum const& frustum,
FScene::LightSoa& lightData) noexcept {
SYSTRACE_CALL();
assert_invariant(lightData.size() > FScene::DIRECTIONAL_LIGHTS_COUNT);
auto const* UTILS_RESTRICT sphereArray = lightData.data<FScene::POSITION_RADIUS>();
auto const* UTILS_RESTRICT directions = lightData.data<FScene::DIRECTION>();
auto const* UTILS_RESTRICT instanceArray = lightData.data<FScene::LIGHT_INSTANCE>();
auto * UTILS_RESTRICT visibleArray = lightData.data<FScene::VISIBILITY>();
Culler::intersects(visibleArray, frustum, sphereArray, lightData.size());
const float4* const UTILS_RESTRICT planes = frustum.getNormalizedPlanes();
// the directional light is considered visible
size_t visibleLightCount = FScene::DIRECTIONAL_LIGHTS_COUNT;
// skip directional light
for (size_t i = FScene::DIRECTIONAL_LIGHTS_COUNT; i < lightData.size(); i++) {
FLightManager::Instance const li = instanceArray[i];
if (visibleArray[i]) {
if (!lcm.isLightCaster(li)) {
visibleArray[i] = 0;
continue;
}
if (lcm.getIntensity(li) <= 0.0f) {
visibleArray[i] = 0;
continue;
}
// cull spotlights that cannot possibly intersect the view frustum
if (lcm.isSpotLight(li)) {
const float3 position = sphereArray[i].xyz;
const float3 axis = directions[i];
const float cosSqr = lcm.getCosOuterSquared(li);
bool invisible = false;
for (size_t j = 0; j < 6; ++j) {
const float p = dot(position + planes[j].xyz * planes[j].w, planes[j].xyz);
const float c = dot(planes[j].xyz, axis);
invisible |= ((1.0f - c * c) < cosSqr && c > 0 && p > 0);
}
if (invisible) {
visibleArray[i] = 0;
continue;
}
}
visibleLightCount++;
}
}
// Partition array such that all visible lights appear first
UTILS_UNUSED_IN_RELEASE auto last =
std::partition(lightData.begin() + FScene::DIRECTIONAL_LIGHTS_COUNT, lightData.end(),
[](auto const& it) {
return it.template get<FScene::VISIBILITY>() != 0;
});
assert_invariant(visibleLightCount == size_t(last - lightData.begin()));
/*
* Some lights might be left out if there are more than the GPU buffer allows (i.e. 256).
*
* We always sort lights by distance to the camera so that:
* - we can build light trees later
* - lights farther from the camera are dropped when in excess
* Note this doesn't always work well, e.g. for search-lights, we might need to also
* take the radius into account.
* - This helps our limited numbers of spot-shadow as well.
*/
// number of point/spotlights
size_t const positionalLightCount = visibleLightCount - FScene::DIRECTIONAL_LIGHTS_COUNT;
if (positionalLightCount) {
assert_invariant(positionalLightCount <= scratch.size());
// pre-compute the lights' distance to the camera, for sorting below
// - we don't skip the directional light, because we don't care, it's ignored during sorting
float* const UTILS_RESTRICT distances = scratch.data();
float4 const* const UTILS_RESTRICT spheres = lightData.data<FScene::POSITION_RADIUS>();
computeLightCameraDistances(distances, viewMatrix, spheres, visibleLightCount);
// skip directional light
Zip2Iterator<FScene::LightSoa::iterator, float*> b = { lightData.begin(), distances };
std::sort(b + FScene::DIRECTIONAL_LIGHTS_COUNT, b + visibleLightCount,
[](auto const& lhs, auto const& rhs) { return lhs.second < rhs.second; });
}
// drop excess lights
lightData.resize(std::min(visibleLightCount,
CONFIG_MAX_LIGHT_COUNT + FScene::DIRECTIONAL_LIGHTS_COUNT));
}
// These methods need to exist so clang honors the __restrict__ keyword, which in turn
// produces much better vectorization. The ALWAYS_INLINE keyword makes sure we actually don't
// pay the price of the call!
UTILS_ALWAYS_INLINE
inline void FView::computeLightCameraDistances(
float* UTILS_RESTRICT const distances,
mat4f const& UTILS_RESTRICT viewMatrix,
float4 const* UTILS_RESTRICT spheres, size_t count) noexcept {
// without this, the vectorization is less efficient
// we're guaranteed to have a multiple of 4 lights (at least)
count = uint32_t(count + 3u) & ~3u;
for (size_t i = 0 ; i < count; i++) {
const float4 sphere = spheres[i];
const float4 center = viewMatrix * sphere.xyz; // camera points towards the -z axis
distances[i] = length(center);
}
}
void FView::updatePrimitivesLod(FEngine& engine, const CameraInfo&,
FScene::RenderableSoa& renderableData, Range visible) noexcept {
FRenderableManager const& rcm = engine.getRenderableManager();
for (uint32_t const index : visible) {
uint8_t const level = 0; // TODO: pick the proper level of detail
auto ri = renderableData.elementAt<FScene::RENDERABLE_INSTANCE>(index);
renderableData.elementAt<FScene::PRIMITIVES>(index) = rcm.getRenderPrimitives(ri, level);
}
}
FrameGraphId<FrameGraphTexture> FView::renderShadowMaps(FEngine& engine, FrameGraph& fg,
CameraInfo const& cameraInfo, float4 const& userTime,
RenderPassBuilder const& passBuilder) noexcept {
assert_invariant(needsShadowMap());
return mShadowMapManager->render(engine, fg, passBuilder, *this, cameraInfo, userTime);
}
void FView::commitFrameHistory(FEngine& engine) noexcept {
// Here we need to destroy resources in mFrameHistory.back()
auto& frameHistory = mFrameHistory;
FrameHistoryEntry& last = frameHistory.back();
last.taa.color.destroy(engine.getResourceAllocator());
last.ssr.color.destroy(engine.getResourceAllocator());
// and then push the new history entry to the history stack
frameHistory.commit();
}
void FView::drainFrameHistory(FEngine& engine) noexcept {
// make sure we free all resources in the history
for (size_t i = 0; i < mFrameHistory.size(); ++i) {
commitFrameHistory(engine);
}
}
void FView::executePickingQueries(backend::DriverApi& driver,
backend::RenderTargetHandle handle, float2 scale) noexcept {
while (mActivePickingQueriesList) {
FPickingQuery* const pQuery = mActivePickingQueriesList;
mActivePickingQueriesList = pQuery->next;
// adjust for dynamic resolution and structure buffer scale
const uint32_t x = uint32_t(float(pQuery->x) * scale.x);
const uint32_t y = uint32_t(float(pQuery->y) * scale.y);
if (UTILS_UNLIKELY(driver.getFeatureLevel() == FeatureLevel::FEATURE_LEVEL_0)) {
driver.readPixels(handle, x, y, 1, 1, {
&pQuery->result.reserved1, 4u, // 4
backend::PixelDataFormat::RGBA, backend::PixelDataType::UBYTE,
pQuery->handler, [](void*, size_t, void* user) {
FPickingQuery* pQuery = static_cast<FPickingQuery*>(user);
uint8_t const* const p =
reinterpret_cast<uint8_t const *>(&pQuery->result.reserved1);
uint32_t const r = p[0];
uint32_t const g = p[1];
uint32_t const b = p[2];
uint32_t const a = p[3];
int32_t const identity = int32_t(a << 16u | (b << 8u) | g);
float const depth = float(r) / 255.0f;
pQuery->result.renderable = Entity::import(identity);
pQuery->result.depth = depth;
pQuery->result.fragCoords = {
pQuery->x, pQuery->y, float(1.0 - depth) };
pQuery->callback(pQuery->result, pQuery);
FPickingQuery::put(pQuery);
}, pQuery
});
} else {
driver.readPixels(handle, x, y, 1, 1, {
&pQuery->result.renderable, 4u * 4u, // 4*float
backend::PixelDataFormat::RG, backend::PixelDataType::FLOAT,
pQuery->handler, [](void*, size_t, void* user) {
FPickingQuery* const pQuery = static_cast<FPickingQuery*>(user);
// pQuery->result.renderable already contains the right value!
pQuery->result.fragCoords = {
pQuery->x, pQuery->y, float(1.0 - pQuery->result.depth) };
pQuery->callback(pQuery->result, pQuery);
FPickingQuery::put(pQuery);
}, pQuery
});
}
}
}
void FView::setTemporalAntiAliasingOptions(TemporalAntiAliasingOptions options) noexcept {
options.feedback = math::clamp(options.feedback, 0.0f, 1.0f);
options.filterWidth = std::max(0.2f, options.filterWidth); // below 0.2 causes issues
mTemporalAntiAliasingOptions = options;
}
void FView::setMultiSampleAntiAliasingOptions(MultiSampleAntiAliasingOptions options) noexcept {
options.sampleCount = uint8_t(options.sampleCount < 1u ? 1u : options.sampleCount);
mMultiSampleAntiAliasingOptions = options;
assert_invariant(!options.enabled || !mRenderTarget || !mRenderTarget->hasSampleableDepth());
}
void FView::setScreenSpaceReflectionsOptions(ScreenSpaceReflectionsOptions options) noexcept {
options.thickness = std::max(0.0f, options.thickness);
options.bias = std::max(0.0f, options.bias);
options.maxDistance = std::max(0.0f, options.maxDistance);
options.stride = std::max(1.0f, options.stride);
mScreenSpaceReflectionsOptions = options;
}
void FView::setGuardBandOptions(GuardBandOptions options) noexcept {
mGuardBandOptions = options;
}
void FView::setAmbientOcclusionOptions(AmbientOcclusionOptions options) noexcept {
options.radius = math::max(0.0f, options.radius);
options.power = std::max(0.0f, options.power);
options.bias = math::clamp(options.bias, 0.0f, 0.1f);
// snap to the closer of 0.5 or 1.0
options.resolution = std::floor(
math::clamp(options.resolution * 2.0f, 1.0f, 2.0f) + 0.5f) * 0.5f;
options.intensity = std::max(0.0f, options.intensity);
options.bilateralThreshold = std::max(0.0f, options.bilateralThreshold);
options.minHorizonAngleRad = math::clamp(options.minHorizonAngleRad, 0.0f, math::f::PI_2);
options.ssct.lightConeRad = math::clamp(options.ssct.lightConeRad, 0.0f, math::f::PI_2);
options.ssct.shadowDistance = std::max(0.0f, options.ssct.shadowDistance);
options.ssct.contactDistanceMax = std::max(0.0f, options.ssct.contactDistanceMax);
options.ssct.intensity = std::max(0.0f, options.ssct.intensity);
options.ssct.lightDirection = normalize(options.ssct.lightDirection);
options.ssct.depthBias = std::max(0.0f, options.ssct.depthBias);
options.ssct.depthSlopeBias = std::max(0.0f, options.ssct.depthSlopeBias);
options.ssct.sampleCount = math::clamp((unsigned)options.ssct.sampleCount, 1u, 255u);
options.ssct.rayCount = math::clamp((unsigned)options.ssct.rayCount, 1u, 255u);
mAmbientOcclusionOptions = options;
}
void FView::setVsmShadowOptions(VsmShadowOptions options) noexcept {
options.msaaSamples = std::max(uint8_t(0), options.msaaSamples);
mVsmShadowOptions = options;
}
void FView::setSoftShadowOptions(SoftShadowOptions options) noexcept {
options.penumbraScale = std::max(0.0f, options.penumbraScale);
options.penumbraRatioScale = std::max(1.0f, options.penumbraRatioScale);
mSoftShadowOptions = options;
}
void FView::setBloomOptions(BloomOptions options) noexcept {
options.dirtStrength = math::saturate(options.dirtStrength);
options.resolution = math::clamp(options.resolution, 2u, 2048u);
options.levels = math::clamp(options.levels, uint8_t(1),
FTexture::maxLevelCount(options.resolution));
options.highlight = std::max(10.0f, options.highlight);
mBloomOptions = options;
}
void FView::setFogOptions(FogOptions options) noexcept {
options.distance = std::max(0.0f, options.distance);
options.maximumOpacity = math::clamp(options.maximumOpacity, 0.0f, 1.0f);
options.density = std::max(0.0f, options.density);
options.heightFalloff = std::max(0.0f, options.heightFalloff);
options.inScatteringSize = options.inScatteringSize;
options.inScatteringStart = std::max(0.0f, options.inScatteringStart);
mFogOptions = options;
}
void FView::setDepthOfFieldOptions(DepthOfFieldOptions options) noexcept {
options.cocScale = std::max(0.0f, options.cocScale);
options.maxApertureDiameter = std::max(0.0f, options.maxApertureDiameter);
mDepthOfFieldOptions = options;
}
void FView::setVignetteOptions(VignetteOptions options) noexcept {
options.roundness = math::saturate(options.roundness);
options.midPoint = math::saturate(options.midPoint);
options.feather = math::clamp(options.feather, 0.05f, 1.0f);
mVignetteOptions = options;
}
View::PickingQuery& FView::pick(uint32_t x, uint32_t y, backend::CallbackHandler* handler,
View::PickingQueryResultCallback callback) noexcept {
FPickingQuery* pQuery = FPickingQuery::get(x, y, handler, callback);
pQuery->next = mActivePickingQueriesList;
mActivePickingQueriesList = pQuery;
return *pQuery;
}
void FView::setStereoscopicOptions(const StereoscopicOptions& options) noexcept {
mStereoscopicOptions = options;
}
void FView::setMaterialGlobal(uint32_t index, float4 const& value) {
ASSERT_PRECONDITION(index < 4, "material global variable index (%u) out of range", +index);
mMaterialGlobals[index] = value;
}
math::float4 FView::getMaterialGlobal(uint32_t index) const {
ASSERT_PRECONDITION(index < 4, "material global variable index (%u) out of range", +index);
return mMaterialGlobals[index];
}
} // namespace filament