Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved picking #494

Merged
merged 8 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 80 additions & 10 deletions examples/picking/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub async fn run() {

let mut camera = Camera::new_perspective(
window.viewport(),
vec3(4.0, 4.0, 5.0),
vec3(2.0, 2.0, 25.0),
vec3(0.0, 0.0, 0.0),
vec3(0.0, 1.0, 0.0),
degrees(45.0),
Expand All @@ -28,13 +28,13 @@ pub async fn run() {
let mut control = OrbitControl::new(*camera.target(), 1.0, 100.0);

let mut sphere = CpuMesh::sphere(8);
sphere.transform(&Mat4::from_scale(0.05)).unwrap();
sphere.transform(&Mat4::from_scale(0.1)).unwrap();
let mut pick_mesh = Gm::new(
Mesh::new(&context, &sphere),
PhysicalMaterial::new_opaque(
&context,
&CpuMaterial {
albedo: Srgba::RED,
albedo: Srgba::new(100, 100, 100, 0),
..Default::default()
},
),
Expand All @@ -48,10 +48,46 @@ pub async fn run() {
.unwrap();

let model = loaded.deserialize("suzanne.obj").unwrap();
let mut monkey = Model::<PhysicalMaterial>::new(&context, &model).unwrap();
monkey
.iter_mut()
.for_each(|m| m.material.render_states.cull = Cull::Back);
let mut monkey: Gm<_, _> = Model::<PhysicalMaterial>::new(&context, &model)
.unwrap()
.remove(0)
.into();
monkey.material.render_states.cull = Cull::Back;
monkey.set_transformation(Mat4::from_translation(vec3(2.0, -2.0, 0.0)));
let original_color = monkey.material.albedo;

let mut cone = Gm::new(
Mesh::new(&context, &CpuMesh::cube()),
PhysicalMaterial {
albedo: Srgba::BLUE,
..Default::default()
},
);
cone.set_transformation(Mat4::from_translation(vec3(-2.0, 2.0, 0.0)));

let transformations: Vec<_> = (-30..30)
.flat_map(|i| {
(-30..30).map(move |j| {
Mat4::from_translation(vec3(i as f32, j as f32, 0.0)) * Mat4::from_scale(3.0)
})
})
.collect();
let no_instances = transformations.len();
let instances = Instances {
transformations,
colors: Some(vec![Srgba::GREEN; no_instances]),
..Default::default()
};
let mut instanced_mesh = Gm::new(
InstancedMesh::new(&context, &instances, &sphere),
PhysicalMaterial::new_opaque(
&context,
&CpuMaterial {
albedo: Srgba::WHITE,
..Default::default()
},
),
);

// main loop
window.render_loop(move |mut frame_input| {
Expand All @@ -64,9 +100,39 @@ pub async fn run() {
} = *event
{
if button == MouseButton::Left {
if let Some(pick) = pick(&context, &camera, position, &monkey) {
pick_mesh.set_transformation(Mat4::from_translation(pick));
// Reset colors and pick mesh position
let mut instances = instances.clone();
instanced_mesh.set_instances(&instances);
monkey.material.albedo = original_color;
cone.material.albedo = Srgba::BLUE;
pick_mesh.set_transformation(Mat4::from_translation(vec3(0.0, 0.0, 0.0)));

// Pick
if let Some(pick) = pick(
&context,
&camera,
position,
monkey.into_iter().chain(&cone).chain(&instanced_mesh),
) {
pick_mesh.set_transformation(Mat4::from_translation(pick.position));
match pick.geometry_id {
0 => {
monkey.material.albedo = Srgba::RED;
}
1 => {
cone.material.albedo = Srgba::RED;
}
2 => {
instances.colors.as_mut().unwrap()[pick.instance_id as usize] =
Srgba::RED;
instanced_mesh.set_instances(&instances);
}
_ => {
unreachable!()
}
};
change = true;
} else {
}
}
}
Expand All @@ -81,7 +147,11 @@ pub async fn run() {
.clear(ClearState::color_and_depth(1.0, 1.0, 1.0, 1.0, 1.0))
.render(
&camera,
monkey.into_iter().chain(&pick_mesh),
monkey
.into_iter()
.chain(&instanced_mesh)
.chain(&cone)
.chain(&pick_mesh),
&[&ambient, &directional],
);
}
Expand Down
42 changes: 26 additions & 16 deletions src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ pub fn pick(
camera: &Camera,
pixel: impl Into<PhysicalPoint> + Copy,
geometries: impl IntoIterator<Item = impl Geometry>,
) -> Option<Vec3> {
) -> Option<IntersectionResult> {
let pos = camera.position_at_pixel(pixel);
let dir = camera.view_direction_at_pixel(pixel);
ray_intersect(
Expand All @@ -600,6 +600,17 @@ pub fn pick(
)
}

/// Result from an intersection test
#[derive(Debug, Clone, Copy)]
pub struct IntersectionResult {
/// The position of the intersection
pub position: Vec3,
/// The index of the intersected geometry in the list of geometries
pub geometry_id: u32,
/// The index of the intersected instance in the list of instances or 0 if the intersection did not hit an instanced geometry
pub instance_id: u32,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recently noticed other 3D rendering libraries that provide this functionality also include the face index in their return result, which would take up the last alpha color slot. I believe this can be obtained from gl_PrimitiveID in the IntersectionMaterial fragment shader. This isn't needed for my use case, but could be beneficial in this hypothetical use case: a button panel, where all the buttons are part of the same model; it is known which triangles make up the buttons, so the face index is used to identify which button was clicked.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point! No reason not to add that to the intersection result as well. Fixed in e390c6a

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out gl_PrimitiveID is not supported on web, so I'm removing it again.


///
/// Finds the closest intersection between a ray starting at the given position in the given direction and the given geometries.
/// Returns ```None``` if no geometry was hit before the given maximum depth.
Expand All @@ -610,7 +621,7 @@ pub fn ray_intersect(
direction: Vec3,
max_depth: f32,
geometries: impl IntoIterator<Item = impl Geometry>,
) -> Option<Vec3> {
) -> Option<IntersectionResult> {
use crate::core::*;
let viewport = Viewport::new_at_origo(1, 1);
let up = if direction.dot(vec3(1.0, 0.0, 0.0)).abs() > 0.99 {
Expand All @@ -627,7 +638,7 @@ pub fn ray_intersect(
0.0,
max_depth,
);
let mut texture = Texture2D::new_empty::<f32>(
let mut texture = Texture2D::new_empty::<[f32; 4]>(
context,
viewport.width,
viewport.height,
Expand All @@ -644,31 +655,30 @@ pub fn ray_intersect(
Wrapping::ClampToEdge,
Wrapping::ClampToEdge,
);
let depth_material = DepthMaterial {
render_states: RenderStates {
write_mask: WriteMask {
red: true,
..WriteMask::DEPTH
},
..Default::default()
},
let mut material = IntersectionMaterial {
..Default::default()
};
let depth = RenderTarget::new(
let result = RenderTarget::new(
texture.as_color_target(None),
depth_texture.as_depth_target(),
)
.clear(ClearState::color_and_depth(1.0, 1.0, 1.0, 1.0, 1.0))
.write::<RendererError>(|| {
for geometry in geometries {
render_with_material(context, &camera, &geometry, &depth_material, &[]);
for (id, geometry) in geometries.into_iter().enumerate() {
material.geometry_id = id as i32;
render_with_material(context, &camera, &geometry, &material, &[]);
}
Ok(())
})
.unwrap()
.read_color::<[f32; 4]>()[0][0];
.read_color::<[f32; 4]>()[0];
let depth = result[0];
if depth < 1.0 {
Some(position + direction * depth * max_depth)
Some(IntersectionResult {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IDs here are limited by the maximum safe integer a f32 can hold (approximately 24 bits). However, use cases where the IDs ever get that high are probably extremely rare, so it probably isn't worth the additional complexity that fixing that issue would incur (what's currently done in #479). Maybe this limitation should be added to the documentation somewhere?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking 24 bit integer would be enough (16,777,215 instances is probably rare 🙂), but if we also have to output primitive ids, it's probably better to support the whole u32/i32 range. I fixed that in 439261b

position: position + direction * depth * max_depth,
geometry_id: result[1] as u32,
instance_id: result[2] as u32,
})
} else {
None
}
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ pub use three_d_asset::{
/// - uv coordinates: `out vec2 uvs;` (must be flipped in v compared to standard uv coordinates, ie. do `uvs = vec2(uvs.x, 1.0 - uvs.y);` in the vertex shader or do the flip before constructing the uv coordinates vertex buffer)
/// - color: `out vec4 col;`
///
/// In addition, for the geometry to be pickable using the [pick] or [ray_intersect] methods (ie. combined with the [IntersectionMaterial]),
/// it needs to support `flat out int instance_id;`. Simply set it to the built-in glsl variable: `gl_InstanceID`.
///
pub trait Geometry {
///
/// Draw this geometry.
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/geometry/shaders/mesh.vert
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ in vec4 instance_color;
#endif

out vec4 col;
flat out int instance_id;

void main()
{
Expand Down Expand Up @@ -117,4 +118,5 @@ void main()
#ifdef USE_INSTANCE_COLORS
col *= instance_color;
#endif
instance_id = gl_InstanceID;
}
2 changes: 2 additions & 0 deletions src/renderer/geometry/shaders/sprites.vert
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ in vec2 uv_coordinate;
out vec2 uvs;
out vec4 col;
out vec3 pos;
flat out int instance_id;

void main()
{
Expand Down Expand Up @@ -38,4 +39,5 @@ void main()
vec4 world_pos = instanced_transform * transformation * vec4(position, 1.);
pos = world_pos.xyz / world_pos.w;
gl_Position = viewProjection * world_pos;
instance_id = gl_InstanceID;
}
4 changes: 4 additions & 0 deletions src/renderer/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ mod depth_material;
#[doc(inline)]
pub use depth_material::*;

mod intersection_material;
#[doc(inline)]
pub use intersection_material::*;

mod normal_material;
#[doc(inline)]
pub use normal_material::*;
Expand Down
67 changes: 67 additions & 0 deletions src/renderer/material/intersection_material.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use crate::core::*;
use crate::renderer::*;

///
/// Used for intersection tests, see [pick] and [ray_intersect].
/// When rendering with this material, the output in each pixel is:
/// Red channel: The depth (same as [DepthMaterial])
/// Green channel: The [IntersectionMaterial::geometry_id]
/// Blue channel: The instance ID or 0, if this is not an instanced geometry
///
/// Note: The geometry needs to pass the instance ID to the fragment shader, see [Geometry] for more information.
///
#[derive(Default, Clone)]
pub struct IntersectionMaterial {
/// The minimum distance from the camera to any object. If None, then the near plane of the camera is used.
pub min_distance: Option<f32>,
/// The maximum distance from the camera to any object. If None, then the far plane of the camera is used.
pub max_distance: Option<f32>,
/// Render states.
pub render_states: RenderStates,
/// A geometry ID for the currently rendered geometry. The result is outputted in the green color channel.
pub geometry_id: i32,
}

impl FromCpuMaterial for IntersectionMaterial {
fn from_cpu_material(_context: &Context, _cpu_material: &CpuMaterial) -> Self {
Self::default()
}
}

impl Material for IntersectionMaterial {
fn id(&self) -> EffectMaterialId {
EffectMaterialId::IntersectionMaterial
}

fn fragment_shader_source(&self, _lights: &[&dyn Light]) -> String {
include_str!("shaders/intersection_material.frag").to_string()
}

fn fragment_attributes(&self) -> FragmentAttributes {
FragmentAttributes {
position: true,
..FragmentAttributes::NONE
}
}

fn use_uniforms(&self, program: &Program, camera: &Camera, _lights: &[&dyn Light]) {
program.use_uniform(
"minDistance",
self.min_distance.unwrap_or_else(|| camera.z_near()),
);
program.use_uniform(
"maxDistance",
self.max_distance.unwrap_or_else(|| camera.z_far()),
);
program.use_uniform("eye", camera.position());
program.use_uniform("geometryId", self.geometry_id);
}

fn render_states(&self) -> RenderStates {
self.render_states
}

fn material_type(&self) -> MaterialType {
MaterialType::Opaque
}
}
16 changes: 16 additions & 0 deletions src/renderer/material/shaders/intersection_material.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

uniform vec3 eye;
uniform float minDistance;
uniform float maxDistance;
uniform int geometryId;

in vec3 pos;
flat in int instance_id;

layout (location = 0) out vec4 outColor;

void main()
{
float dist = (distance(pos, eye) - minDistance) / (maxDistance - minDistance);
outColor = vec4(dist, geometryId, instance_id, 1.0);
}
2 changes: 2 additions & 0 deletions src/renderer/object/shaders/terrain.vert
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ in vec3 position;
out vec3 pos;
out vec2 uvs;
out vec4 col;
flat out int instance_id;

#ifdef USE_NORMALS

Expand All @@ -27,4 +28,5 @@ void main()
bitang = cross(nor, tang);
#endif
gl_Position = viewProjectionMatrix * worldPos;
instance_id = gl_InstanceID;
}
2 changes: 2 additions & 0 deletions src/renderer/object/shaders/water.vert
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ out vec2 uvs;
out vec3 nor;
out vec3 pos;
out vec4 col;
flat out int instance_id;

void main()
{
Expand Down Expand Up @@ -54,4 +55,5 @@ void main()
gl_Position = viewProjection * vec4(pos, 1.);
uvs = pos.xz;
col = vec4(1.0);
instance_id = gl_InstanceID;
}
1 change: 1 addition & 0 deletions src/renderer/shader_ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ pub enum EffectMaterialId {
SkyboxMaterial = 0x8004,
UVMaterial = 0x8005,
NormalMaterialBase = 0x8006, // To 0x8007
IntersectionMaterial = 0x800B,
IsosurfaceMaterial = 0x800C,
ImpostersMaterial = 0x800D,
BrdfMaterial = 0x800E,
Expand Down
Loading