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.

The depth buffer value is retrieved and the fragment coordinate is
reconstructed and passed to PickingQueryResult. This can be used in
turn to calculate the view and/or world space position.
  • Loading branch information
pixelflinger committed Sep 29, 2021
1 parent b71ee48 commit 7d80052
Show file tree
Hide file tree
Showing 23 changed files with 506 additions and 96 deletions.
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ A new header is inserted each time a *tag* is created.

## v1.12.8 (currently main branch)

- engine: Added picking API to `View` [⚠️ **Materials need to be rebuilt to access this new feature**].

## v1.12.7

## v1.12.6
Expand Down
3 changes: 3 additions & 0 deletions android/common/CallbackUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ struct JniCallback {

static void invoke(void* user);

jobject getCallbackObject() { return mCallback; }
JNIEnv* getJniEnv() { return mEnv; }

private:
JniCallback(JNIEnv* env, jobject handler, jobject runnable);
JniCallback(JniCallback const &) = delete;
Expand Down
40 changes: 40 additions & 0 deletions android/filament-android/src/main/cpp/View.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#include <filament/View.h>
#include <filament/Viewport.h>

#include "common/CallbackUtils.h"

using namespace filament;

extern "C" JNIEXPORT void JNICALL
Expand Down Expand Up @@ -382,3 +384,41 @@ Java_com_google_android_filament_View_nIsScreenSpaceRefractionEnabled(JNIEnv *,
View* view = (View*) nativeView;
return (jboolean)view->isScreenSpaceRefractionEnabled();
}

extern "C"
JNIEXPORT void JNICALL
Java_com_google_android_filament_View_nPick(JNIEnv* env, jclass clazz,
jlong nativeView,
jint x, jint y, jobject handler, jobject internalCallback) {

// jniState will be initialized the first time this method is called
static const struct JniState {
jclass internalOnPickCallbackClass;
jfieldID renderableFieldId;
jfieldID depthFieldId;
jfieldID fragCoordXFieldId;
jfieldID fragCoordYFieldId;
jfieldID fragCoordZFieldId;
explicit JniState(JNIEnv* env) noexcept {
internalOnPickCallbackClass = env->FindClass("com/google/android/filament/View$InternalOnPickCallback");
renderableFieldId = env->GetFieldID(internalOnPickCallbackClass, "mRenderable", "I");
depthFieldId = env->GetFieldID(internalOnPickCallbackClass, "mDepth", "F");
fragCoordXFieldId = env->GetFieldID(internalOnPickCallbackClass, "mFragCoordsX", "F");
fragCoordYFieldId = env->GetFieldID(internalOnPickCallbackClass, "mFragCoordsY", "F");
fragCoordZFieldId = env->GetFieldID(internalOnPickCallbackClass, "mFragCoordsZ", "F");
}
} jniState(env);

View* view = (View*) nativeView;
JniCallback *callback = JniCallback::make(env, handler, internalCallback);
view->pick(x, y, [callback](View::PickingQueryResult const& result) {
jobject obj = callback->getCallbackObject();
JNIEnv* const env = callback->getJniEnv();
env->SetIntField(obj, jniState.renderableFieldId, (jint)result.renderable.getId());
env->SetFloatField(obj, jniState.depthFieldId, result.depth);
env->SetFloatField(obj, jniState.fragCoordXFieldId, result.fragCoords.x);
env->SetFloatField(obj, jniState.fragCoordYFieldId, result.fragCoords.y);
env->SetFloatField(obj, jniState.fragCoordZFieldId, result.fragCoords.z);
JniCallback::invoke(callback); // this destroys JniCallback
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,71 @@ public DepthOfFieldOptions getDepthOfFieldOptions() {
return mDepthOfFieldOptions;
}

/**
* A class containing the result of a picking query
*/
public static class PickingQueryResult {
/** The entity of the renderable at the picking query location */
@Entity public int renderable;
/** The value of the depth buffer at the picking query location */
public float depth;
/** The fragment coordinate in GL convention at the the picking query location */
@NonNull public float[] fragCoords = new float[3];
};

/**
* An interface to implement a custom class to receive results of picking queries.
*/
public interface OnPickCallback {
/**
* onPick() is called by the specified Handler in {@link View#pick} when the picking query
* result is available.
* @param result An instance of {@link PickingQueryResult}.
*/
void onPick(@NonNull PickingQueryResult result);
}

/**
* Creates a picking query. Multiple queries can be created (e.g.: multi-touch).
* Picking queries are all executed when {@link 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 handler An {@link java.util.concurrent.Executor Executor}.
* On Android this can also be a {@link android.os.Handler Handler}.
* @param callback User callback executed by <code>handler</code> when the picking query
* result is available.
*/
public void pick(int x, int y,
@Nullable Object handler, @Nullable OnPickCallback callback) {
InternalOnPickCallback internalCallback = new InternalOnPickCallback(callback);
nPick(getNativeObject(), x, y, handler, internalCallback);
}

private static class InternalOnPickCallback implements Runnable {
public InternalOnPickCallback(OnPickCallback mUserCallback) {
this.mUserCallback = mUserCallback;
}
@Override
public void run() {
mPickingQueryResult.renderable = mRenderable;
mPickingQueryResult.depth = mDepth;
mPickingQueryResult.fragCoords[0] = mFragCoordsX;
mPickingQueryResult.fragCoords[1] = mFragCoordsY;
mPickingQueryResult.fragCoords[2] = mFragCoordsZ;
mUserCallback.onPick(mPickingQueryResult);
}
private final OnPickCallback mUserCallback;
private final PickingQueryResult mPickingQueryResult = new PickingQueryResult();
@Entity int mRenderable;
float mDepth;
float mFragCoordsX;
float mFragCoordsY;
float mFragCoordsZ;
}

public long getNativeObject() {
if (mNativeObject == 0) {
Expand Down Expand Up @@ -1598,4 +1663,5 @@ private static native void nSetDepthOfFieldOptions(long nativeView, float cocSca
private static native boolean nIsShadowingEnabled(long nativeView);
private static native void nSetScreenSpaceRefractionEnabled(long nativeView, boolean enabled);
private static native boolean nIsScreenSpaceRefractionEnabled(long nativeView);
private static native void nPick(long nativeView, int x, int y, Object handler, InternalOnPickCallback internalCallback);
}
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 @@ -2865,8 +2866,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
112 changes: 111 additions & 1 deletion 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,116 @@ 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 {
utils::Entity renderable{}; //! RenderableManager Entity at the queried coordinates
float depth{}; //! Depth buffer value (1 (near plane) to 0 (infinity))
uint32_t reserved1{};
uint32_t reserved2{};
/**
* screen space coordinates in GL convention, this can be used to compute the view or
* world space position of the picking hit. For e.g.:
* clip_space_position = (fragCoords.xy / viewport.wh, fragCoords.z) * 2.0 - 1.0
* view_space_position = inverse(projection) * clip_space_position
* world_space_position = model * view_space_position
*
* The viewport, projection and model matrices can be obtained from Camera. Because
* pick() has some latency, it might be more accurate to obtain these values at the
* time the View::pick() call is made.
*/
math::float3 fragCoords; //! screen space coordinates in GL convention
};

/** User data for PickingQueryResultCallback */
struct PickingQuery {
// note: this is enough to store a std::function<> -- just saying...
void* storage[4];
};

/** 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.: 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.: 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.: 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 Expand Up @@ -601,7 +712,6 @@ class UTILS_PUBLIC View : public FilamentAPI {
AmbientOcclusion getAmbientOcclusion() const noexcept;
};


} // namespace filament

#endif // TNT_FILAMENT_VIEW_H
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::RG32UI });

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
Loading

0 comments on commit 7d80052

Please sign in to comment.