Skip to content

Commit

Permalink
Implement picking functionality
Browse files Browse the repository at this point in the history
This is a pixel accurate implementation of picking. Picking queries
can be created on view, and upon completion a user provided callback
is called with the Entity of the renderable at the queried coordinates
in the viewport.


Picking queries typically have 1 or 2 frame of latency and may impact
performance on some drivers.

It is mostly intended for use by editors, or when latency is not a major
concern. This api should not be used for dragging/moving objects, it is
intended for initial picking only.


Picking is implemented in the structure pass.
  • Loading branch information
pixelflinger committed Sep 25, 2021
1 parent 636ea5a commit 1c6a2c6
Show file tree
Hide file tree
Showing 18 changed files with 378 additions and 95 deletions.
3 changes: 1 addition & 2 deletions filament/backend/src/opengl/OpenGLDriver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2834,6 +2834,7 @@ void OpenGLDriver::readPixels(Handle<HwRenderTarget> src,
glBufferData(GL_PIXEL_PACK_BUFFER, p.size, nullptr, GL_STATIC_DRAW);
glReadPixels(GLint(x), GLint(y), GLint(width), GLint(height), glFormat, glType, nullptr);
gl.bindBuffer(GL_PIXEL_PACK_BUFFER, 0);
CHECK_GL_ERROR(utils::slog.e)

// we're forced to make a copy on the heap because otherwise it deletes std::function<> copy
// constructor.
Expand Down Expand Up @@ -2866,8 +2867,6 @@ void OpenGLDriver::readPixels(Handle<HwRenderTarget> src,
delete pUserBuffer;
CHECK_GL_ERROR(utils::slog.e)
});

CHECK_GL_ERROR(utils::slog.e)
}

void OpenGLDriver::whenGpuCommandsComplete(std::function<void()> fn) noexcept {
Expand Down
99 changes: 99 additions & 0 deletions filament/include/filament/View.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <backend/DriverEnums.h>

#include <utils/compiler.h>
#include <utils/Entity.h>

#include <math/mathfwd.h>

Expand Down Expand Up @@ -571,6 +572,104 @@ class UTILS_PUBLIC View : public FilamentAPI {
//! debugging: returns a Camera from the point of view of *the* dominant directional light used for shadowing.
Camera const* getDirectionalLightCamera() const noexcept;


/** Result of a picking query */
struct PickingQueryResult {
struct {
utils::Entity renderable{}; //! RenderableManager Entity at the queried coordinates
uint32_t reserved0{};
uint32_t reserved1{};
uint32_t reserved2{};
};
};

/** User data for PickingQueryResultCallback */
struct PickingQuery {
void* storage[8];
};

/** callback type used for picking queries. */
using PickingQueryResultCallback = void(*)(PickingQueryResult const& result, PickingQuery* pq);

/**
* Helper for creating a picking query from Foo::method, by pointer.
* e.g.: auto* query = pick<Foo, &Foo::bar>(x, y, &foo);
* @tparam T Class of the method to call (e.g.: Foo)
* @tparam method Method to call on T (e.g.: &Foo::bar)
* @param x Horizontal coordinate to query in the viewport with origin on the left.
* @param y Vertical coordinate to query on the viewport with origin at the bottom.
* @param data A pointer to an instance of T
*/
template<typename T, void(T::*method)(PickingQueryResult const&)>
void pick(uint32_t x, uint32_t y, T* instance) noexcept {
PickingQuery& query = pick(x, y, [](PickingQueryResult const& result, PickingQuery* pq) {
void* user = pq->storage;
(*static_cast<T**>(user)->*method)(result);
});
query.storage[0] = instance;
}

/**
* Helper for creating a picking query from Foo::method, by copy for a small object
* e.g.: auto* query = pick<Foo, &Foo::bar>(x, y, foo);
*
* @tparam T Class of the method to call (e.g.: Foo)
* @tparam method Method to call on T (e.g.: &Foo::bar)
* @param x Horizontal coordinate to query in the viewport with origin on the left.
* @param y Vertical coordinate to query on the viewport with origin at the bottom.
* @param data An instance of T
*/
template<typename T, void(T::*method)(PickingQueryResult const&)>
void pick(uint32_t x, uint32_t y, T instance) noexcept {
static_assert(sizeof(instance) <= sizeof(PickingQuery::storage), "user data too large");
PickingQuery& query = pick(x, y, [](PickingQueryResult const& result, PickingQuery* pq) {
void* user = pq->storage;
T* that = static_cast<T*>(user);
(that->*method)(result);
that->~T();
});
new(query.storage) T(std::move(instance));
}

/**
* Helper for creating a picking query from a small functor
* e.g.: auto* query = pick(x, y, [](PickingQueryResult const& result){});
*
* @param x Horizontal coordinate to query in the viewport with origin on the left.
* @param y Vertical coordinate to query on the viewport with origin at the bottom.
* @param functor A functor, typically a lambda function.
*/
template<typename T>
void pick(uint32_t x, uint32_t y, T functor) noexcept {
static_assert(sizeof(functor) <= sizeof(PickingQuery::storage), "functor too large");
PickingQuery& query = pick(x, y,
(PickingQueryResultCallback)[](PickingQueryResult const& result, PickingQuery* pq) {
void* user = pq->storage;
T& that = *static_cast<T*>(user);
that(result);
that.~T();
});
new(query.storage) T(std::move(functor));
}

/**
* Creates a picking query. Multiple queries can be created (e.g.: multi-touch).
* Picking queries are all executed when Renderer::render() is called on this View.
* The provided callback is guaranteed to be called at some point in the future.
*
* Typically it takes a couple frames to receive the result of a picking query.
*
* @param x Horizontal coordinate to query in the viewport with origin on the left.
* @param y Vertical coordinate to query on the viewport with origin at the bottom.
* @param callback User callback, called when the picking query result is available.
* @return A reference to a PickingQuery structure, which can be used to store up to
* 8*sizeof(void*) bytes of user data. This user data is later accessible
* in the PickingQueryResultCallback callback 3rd parameter.
*/
PickingQuery& pick(uint32_t x, uint32_t y,
PickingQueryResultCallback callback) noexcept;


/**
* List of available ambient occlusion techniques
* @deprecated use AmbientOcclusionOptions::enabled instead
Expand Down
6 changes: 3 additions & 3 deletions filament/src/Material.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ FMaterial::FMaterial(FEngine& engine, const Material::Builder& builder)
if (UTILS_UNLIKELY(!mIsDefaultMaterial && !mHasCustomDepthShader)) {
auto& cachedPrograms = mCachedPrograms;
for (uint8_t i = 0, n = cachedPrograms.size(); i < n; ++i) {
if (Variant(i).isDepthPass()) {
if (Variant::isValidDepthVariant(i)) {
cachedPrograms[i] = engine.getDefaultMaterial()->getProgram(i);
}
}
Expand Down Expand Up @@ -430,7 +430,7 @@ Program FMaterial::getProgramBuilderWithVariants(

ASSERT_POSTCONDITION(isNoop || (fsOK && fsBuilder.size() > 0),
"The material '%s' has not been compiled to include the required "
"GLSL or SPIR-V chunks for the fragment shader (variant=0x%x, filterer=0x%x).",
"GLSL or SPIR-V chunks for the fragment shader (variant=0x%x, filtered=0x%x).",
mName.c_str(), variantKey, fragmentVariantKey);

Program pb;
Expand Down Expand Up @@ -543,7 +543,7 @@ void FMaterial::destroyPrograms(FEngine& engine) {
if (!mIsDefaultMaterial) {
// The depth variants may be shared with the default material, in which case
// we should not free it now.
bool isSharedVariant = Variant(i).isDepthPass() && !mHasCustomDepthShader;
bool isSharedVariant = Variant::isValidDepthVariant(i) && !mHasCustomDepthShader;
if (isSharedVariant) {
// we don't own this variant, skip.
continue;
Expand Down
25 changes: 21 additions & 4 deletions filament/src/PostProcessManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -313,15 +313,18 @@ void PostProcessManager::commitAndRender(FrameGraphResources::RenderPassInfo con
// ------------------------------------------------------------------------------------------------

FrameGraphId<FrameGraphTexture> PostProcessManager::structure(FrameGraph& fg,
RenderPass const& pass, uint32_t width, uint32_t height, float scale) noexcept {
RenderPass const& pass, uint32_t width, uint32_t height,
StructurePassConfig const& config) noexcept {

const float scale = config.scale;

// structure pass -- automatically culled if not used, currently used by:
// - ssao
// - contact shadows
// It consists of a mipmapped depth pass, tuned for SSAO
struct StructurePassData {
FrameGraphId<FrameGraphTexture> depth;
FrameGraphId<FrameGraphTexture> picking;
};

// sanitize a bit the user provided scaling factor
Expand All @@ -340,11 +343,24 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::structure(FrameGraph& fg,
.levels = uint8_t(levelCount),
.format = TextureFormat::DEPTH32F });

data.depth = builder.write(data.depth, FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
// workaround: since we have levels, this implies SAMPLEABLE (because of the gl
// backend, which implements non-sampleables with renderbuffers, which don't have levels).
// (should the gl driver revert to textures, in that case?)
data.depth = builder.write(data.depth,
FrameGraphTexture::Usage::DEPTH_ATTACHMENT | FrameGraphTexture::Usage::SAMPLEABLE);

if (config.picking) {
data.picking = builder.createTexture("Picking Buffer", {
.width = width, .height = height,
.format = TextureFormat::R32UI });

data.picking = builder.write(data.picking,
FrameGraphTexture::Usage::COLOR_ATTACHMENT);
}

builder.declareRenderPass("Structure Target", {
.attachments = { .depth = data.depth },
.clearFlags = TargetBufferFlags::DEPTH
.attachments = { .color = { data.picking }, .depth = data.depth },
.clearFlags = TargetBufferFlags::COLOR0 | TargetBufferFlags::DEPTH
});
},
[=](FrameGraphResources const& resources,
Expand Down Expand Up @@ -393,6 +409,7 @@ FrameGraphId<FrameGraphTexture> PostProcessManager::structure(FrameGraph& fg,
});

fg.getBlackboard().put("structure", depth);
fg.getBlackboard().put("picking", structurePass->picking);
return depth;
}

Expand Down
9 changes: 7 additions & 2 deletions filament/src/PostProcessManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,18 @@ struct CameraInfo;
class PostProcessManager {
public:
struct ColorGradingConfig {
bool asSubpass = false;
bool asSubpass{};
bool translucent{};
bool fxaa{};
bool dithering{};
backend::TextureFormat ldrFormat{};
};

struct StructurePassConfig {
float scale = 0.5f;
bool picking{};
};

explicit PostProcessManager(FEngine& engine) noexcept;

void init() noexcept;
Expand All @@ -65,7 +70,7 @@ class PostProcessManager {

// structure (depth) pass
FrameGraphId<FrameGraphTexture> structure(FrameGraph& fg, RenderPass const& pass,
uint32_t width, uint32_t height, float scale) noexcept;
uint32_t width, uint32_t height, StructurePassConfig const& config) noexcept;

// SSAO
FrameGraphId<FrameGraphTexture> screenSpaceAmbientOcclusion(FrameGraph& fg,
Expand Down
46 changes: 25 additions & 21 deletions filament/src/RenderPass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,8 @@ void RenderPass::generateCommandsImpl(uint32_t extraFlags,
// (in principle, we could have split this method into two, at the cost of going through
// the list twice)

const bool isColorPass = bool(commandTypeFlags & CommandTypeFlags::COLOR);
const bool isDepthPass = bool(commandTypeFlags & CommandTypeFlags::DEPTH);
constexpr bool isColorPass = bool(commandTypeFlags & CommandTypeFlags::COLOR);
constexpr bool isDepthPass = bool(commandTypeFlags & CommandTypeFlags::DEPTH);

static_assert(isColorPass != isDepthPass, "only color or depth pass supported");

Expand All @@ -292,13 +292,16 @@ void RenderPass::generateCommandsImpl(uint32_t extraFlags,
Command cmdColor;

Command cmdDepth;
cmdDepth.primitive.materialVariant = Variant{ Variant::DEPTH_VARIANT };
cmdDepth.primitive.materialVariant.setVsm(renderFlags & HAS_VSM);
cmdDepth.primitive.rasterState = {};
cmdDepth.primitive.rasterState.colorWrite = renderFlags & HAS_VSM;
cmdDepth.primitive.rasterState.depthWrite = true;
cmdDepth.primitive.rasterState.depthFunc = RasterState::DepthFunc::GE;
cmdDepth.primitive.rasterState.alphaToCoverage = false;
if constexpr (isDepthPass) {
cmdDepth.primitive.materialVariant = Variant{ Variant::DEPTH_VARIANT };
cmdDepth.primitive.materialVariant.setPicking(renderFlags & HAS_PICKING);
cmdDepth.primitive.materialVariant.setVsm(renderFlags & HAS_VSM);
cmdDepth.primitive.rasterState = {};
cmdDepth.primitive.rasterState.colorWrite = renderFlags & (HAS_VSM | HAS_PICKING);
cmdDepth.primitive.rasterState.depthWrite = true;
cmdDepth.primitive.rasterState.depthFunc = RasterState::DepthFunc::GE;
cmdDepth.primitive.rasterState.alphaToCoverage = false;
}

for (uint32_t i = range.first; i < range.last; ++i) {
// Check if this renderable passes the visibilityMask. If it doesn't, encode SENTINEL
Expand Down Expand Up @@ -353,15 +356,16 @@ void RenderPass::generateCommandsImpl(uint32_t extraFlags,
materialVariant.setShadowReceiver(soaVisibility[i].receiveShadows & hasShadowing);
materialVariant.setSkinning(soaVisibility[i].skinning || soaVisibility[i].morphing);

// we're assuming we're always doing the depth (either way, it's correct)
// this will generate front to back rendering
cmdDepth.key = uint64_t(Pass::DEPTH);
cmdDepth.key |= uint64_t(CustomCommand::PASS);
cmdDepth.key |= makeField(soaVisibility[i].priority, PRIORITY_MASK, PRIORITY_SHIFT);
cmdDepth.key |= makeField(distanceBits, DISTANCE_BITS_MASK, DISTANCE_BITS_SHIFT);
cmdDepth.primitive.index = (uint16_t)i;
cmdDepth.primitive.materialVariant.setSkinning(soaVisibility[i].skinning || soaVisibility[i].morphing);
cmdDepth.primitive.rasterState.inverseFrontFaces = inverseFrontFaces;
if constexpr (isDepthPass) {
cmdDepth.key = uint64_t(Pass::DEPTH);
cmdDepth.key |= uint64_t(CustomCommand::PASS);
cmdDepth.key |= makeField(soaVisibility[i].priority, PRIORITY_MASK, PRIORITY_SHIFT);
cmdDepth.key |= makeField(distanceBits, DISTANCE_BITS_MASK, DISTANCE_BITS_SHIFT);
cmdDepth.primitive.index = (uint16_t)i;
cmdDepth.primitive.materialVariant.setSkinning(
soaVisibility[i].skinning || soaVisibility[i].morphing);
cmdDepth.primitive.rasterState.inverseFrontFaces = inverseFrontFaces;
}

const bool shadowCaster = soaVisibility[i].castShadows & hasShadowing;
const bool writeDepthForShadowCasters = depthContainsShadowCasters & shadowCaster;
Expand All @@ -374,7 +378,7 @@ void RenderPass::generateCommandsImpl(uint32_t extraFlags,
*/
for (auto const& primitive : primitives) {
FMaterialInstance const* const mi = primitive.getMaterialInstance();
if (isColorPass) {
if constexpr (isColorPass) {
cmdColor.primitive.primitiveHandle = primitive.getHwHandle();
cmdColor.primitive.materialVariant = materialVariant;
RenderPass::setupColorCommand(cmdColor, mi, inverseFrontFaces);
Expand Down Expand Up @@ -454,13 +458,13 @@ void RenderPass::generateCommandsImpl(uint32_t extraFlags,
++curr;
}

if (isDepthPass) {
if constexpr (isDepthPass) {
FMaterial const* const ma = mi->getMaterial();
const RasterState rs = ma->getRasterState();
const TransparencyMode mode = mi->getTransparencyMode();
const BlendingMode blendingMode = ma->getBlendingMode();
const bool translucent = (blendingMode != BlendingMode::OPAQUE
&& blendingMode != BlendingMode::MASKED);
&& blendingMode != BlendingMode::MASKED);

// unconditionally write the command
cmdDepth.primitive.primitiveHandle = primitive.getHwHandle();
Expand Down
3 changes: 2 additions & 1 deletion filament/src/RenderPass.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ class RenderPass {
static constexpr RenderFlags HAS_INVERSE_FRONT_FACES = 0x08;
static constexpr RenderFlags HAS_FOG = 0x10;
static constexpr RenderFlags HAS_VSM = 0x20;
static constexpr RenderFlags HAS_PICKING = 0x40;

// Arena used for commands
using Arena = utils::Arena<
Expand Down Expand Up @@ -270,7 +271,7 @@ class RenderPass {
// specifies camera information (e.g. used for sorting commands)
void setCamera(const CameraInfo& camera) noexcept { mCamera = camera; }

// flags controling how commands are generated
// flags controlling how commands are generated
void setRenderFlags(RenderFlags flags) noexcept { mFlags = flags; }

// Sets the visibility mask, which is AND-ed against each Renderable's VISIBLE_MASK to determine
Expand Down
Loading

0 comments on commit 1c6a2c6

Please sign in to comment.