WIP: Test Embree alternative to Blender BVH trees #108148

Draft
Lukas Tönne wants to merge 12 commits from LukasTonne/blender:bvh-embree into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
16 changed files with 1130 additions and 65 deletions

View File

@ -590,6 +590,7 @@ endif()
option(WITH_BOOST "Enable features depending on boost" ON)
option(WITH_TBB "Enable multithreading. TBB is also required for features such as Cycles, OpenVDB and USD" ON)
option(WITH_BVH_EMBREE "Enable Embree support for BVH trees" OFF)
# TBB malloc is only supported on for windows currently
if(WIN32)
@ -887,6 +888,7 @@ set_and_warn_dependency(WITH_BOOST WITH_INTERNATIONAL OFF)
set_and_warn_dependency(WITH_BOOST WITH_OPENVDB OFF)
set_and_warn_dependency(WITH_BOOST WITH_QUADRIFLOW OFF)
set_and_warn_dependency(WITH_BOOST WITH_USD OFF)
set_and_warn_dependency(WITH_BOOST WITH_BVH_EMBREE OFF)
if(WITH_CYCLES)
set_and_warn_dependency(WITH_BOOST WITH_CYCLES_OSL OFF)
set_and_warn_dependency(WITH_PUGIXML WITH_CYCLES_OSL OFF)
@ -1900,6 +1902,7 @@ if(FIRST_RUN)
info_cfg_text("Build Options:")
info_cfg_option(WITH_ALEMBIC)
info_cfg_option(WITH_BULLET)
info_cfg_option(WITH_BVH_EMBREE)
info_cfg_option(WITH_CLANG)
info_cfg_option(WITH_CYCLES)
info_cfg_option(WITH_FFTW3)

View File

@ -222,6 +222,7 @@ class NODE_MT_geometry_node_GEO_GEOMETRY_SAMPLE(Menu):
node_add_menu.add_node_type(layout, "GeometryNodeProximity")
node_add_menu.add_node_type(layout, "GeometryNodeIndexOfNearest")
node_add_menu.add_node_type(layout, "GeometryNodeRaycast")
node_add_menu.add_node_type(layout, "GeometryNodeRaycastEmbree")
node_add_menu.add_node_type(layout, "GeometryNodeSampleIndex")
node_add_menu.add_node_type(layout, "GeometryNodeSampleNearest")
node_add_menu.draw_assets_for_catalog(layout, self.bl_label)

View File

@ -0,0 +1,80 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_vector_types.hh"
struct Mesh;
struct RTCDeviceTy;
struct RTCSceneTy;
namespace blender::bvh {
struct BVHRay {
static constexpr unsigned int MASK_FULL = 0xffffffff;
static constexpr unsigned int FLAGS_NONE = 0;
/* Coordinate of ray origin */
float3 origin = float3(0.0f);
/* Start of ray segment relative to ray length */
float dist_min = 0.0f;
/* Ray direction */
float3 direction = float3(1.0f, 0.0f, 0.0f);
/* Time of this ray for motion blur */
float time = 0.0f;
/* End of ray segment relative to ray length (set to hit distance) */
float dist_max = 1.0f;
/* Ray mask */
unsigned int mask = MASK_FULL;
/* Ray ID */
unsigned int id = 0;
/* Ray flags */
unsigned int flags = FLAGS_NONE;
};
struct BVHHit {
static constexpr unsigned int INVALID_INSTANCE_ID = 0xffffffff;
static constexpr int MAX_INSTANCE_LEVEL = 8;
/* Geometry normal */
float3 normal;
/* Barycentric UV of hit */
float2 uv;
/* Primitive ID */
unsigned int primitive_id;
/* Geometry ID */
unsigned int geometry_id;
/* Instance ID */
unsigned int instance_id[MAX_INSTANCE_LEVEL];
};
struct BVHRayHit {
BVHRay ray;
BVHHit hit;
};
class BVHTree {
private:
RTCDeviceTy *rtc_device = nullptr;
RTCSceneTy *rtc_scene = nullptr;
public:
BVHTree();
BVHTree(const BVHTree &) = delete;
~BVHTree();
BVHTree &operator=(const BVHTree &) = delete;
void free();
void build_single_mesh(const Mesh &mesh);
bool ray_intersect1(const BVHRay &ray, BVHRayHit &r_hit) const;
};
} // namespace blender

View File

@ -16,6 +16,7 @@
#include "DNA_meshdata_types.h"
#include "BKE_attribute.h"
#include "BKE_attribute_math.hh"
#include "BKE_geometry_fields.hh"
struct Mesh;

View File

@ -22,6 +22,8 @@
# include "DNA_meshdata_types.h"
# include "BKE_bvh.hh"
struct BVHCache;
struct EditMeshData;
struct Mesh;
@ -124,6 +126,9 @@ struct MeshRuntime {
/** Cache for BVH trees generated for the mesh. Defined in 'BKE_bvhutil.c' */
BVHCache *bvh_cache = nullptr;
/** Cache for derived triangulation of the mesh, accessed with #Mesh::looptris(). */
SharedCache<blender::bvh::BVHTree> bvh_embree_cache;
/** Cache of non-manifold boundary data for Shrink-wrap Target Project. */
ShrinkwrapBoundaryData *shrinkwrap_data = nullptr;

View File

@ -1307,6 +1307,7 @@ void BKE_nodetree_remove_layer_n(struct bNodeTree *ntree, struct Scene *scene, i
#define GEO_NODE_SIMULATION_OUTPUT 2101
#define GEO_NODE_INPUT_SIGNED_DISTANCE 2102
#define GEO_NODE_SAMPLE_VOLUME 2103
#define GEO_NODE_RAYCAST_EMBREE 2104
/** \} */

View File

@ -86,6 +86,7 @@ set(SRC
intern/boids.c
intern/bpath.c
intern/brush.cc
intern/bvh.cc
intern/bvhutils.cc
intern/cachefile.c
intern/callbacks.c
@ -339,6 +340,7 @@ set(SRC
BKE_boids.h
BKE_bpath.h
BKE_brush.h
BKE_bvh.hh
BKE_bvhutils.h
BKE_cachefile.h
BKE_callbacks.h
@ -782,6 +784,16 @@ if(WITH_XR_OPENXR)
add_definitions(-DWITH_XR_OPENXR)
endif()
if(WITH_BVH_EMBREE)
add_definitions(-DWITH_BVH_EMBREE)
list(APPEND INC_SYS
${EMBREE_INCLUDE_DIRS}
)
list(APPEND LIB
${EMBREE_LIBRARIES}
)
endif()
if(WITH_TBB)
add_definitions(-DWITH_TBB)
@ -821,6 +833,7 @@ if(WITH_GTESTS)
intern/armature_test.cc
intern/asset_metadata_test.cc
intern/bpath_test.cc
intern/bvh_test.cc
intern/cryptomatte_test.cc
intern/curves_geometry_test.cc
intern/fcurve_test.cc

View File

@ -0,0 +1,274 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_bvh.hh"
#include "BLI_assert.h"
#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"
#include "BKE_mesh.h"
#include "BKE_mesh_runtime.h"
#ifdef WITH_BVH_EMBREE
# include <embree3/rtcore.h>
# include <embree3/rtcore_geometry.h>
# include <embree3/rtcore_ray.h>
# include <embree3/rtcore_scene.h>
namespace blender::bvh {
BVHTree::BVHTree() {}
BVHTree::~BVHTree()
{
free();
}
static void rtc_error_func(void *, enum RTCError, const char *str) {}
static bool rtc_memory_monitor_func(void *userPtr, const ssize_t bytes, const bool)
{
return true;
}
static bool rtc_progress_func(void *user_ptr, const double n)
{
return true;
}
void BVHTree::free()
{
rtcReleaseScene(rtc_scene);
rtc_scene = nullptr;
rtcReleaseDevice(rtc_device);
rtc_device = nullptr;
}
namespace {
struct BvhBuildContext {
RTCDevice device;
RTCScene scene;
RTCBuildQuality build_quality;
};
void set_tri_vertex_buffer(RTCGeometry geom_id, Span<float3> positions, const bool update)
{
// const Attribute *attr_mP = NULL;
size_t num_motion_steps = 1;
// int t_mid = 0;
// if (mesh->has_motion_blur()) {
// attr_mP = mesh->attributes.find(ATTR_STD_MOTION_VERTEX_POSITION);
// if (attr_mP) {
// num_motion_steps = mesh->get_motion_steps();
// t_mid = (num_motion_steps - 1) / 2;
// if (num_motion_steps > RTC_MAX_TIME_STEP_COUNT) {
// assert(0);
// num_motion_steps = RTC_MAX_TIME_STEP_COUNT;
// }
// }
// }
const int num_verts = positions.size();
for (int t = 0; t < num_motion_steps; ++t) {
const float3 *verts;
// if (t == t_mid) {
verts = positions.data();
//}
// else {
// int t_ = (t > t_mid) ? (t - 1) : t;
// verts = &attr_mP->data_float3()[t_ * num_verts];
//}
float *rtc_verts = (update) ?
(float *)rtcGetGeometryBufferData(geom_id, RTC_BUFFER_TYPE_VERTEX, t) :
(float *)rtcSetNewGeometryBuffer(geom_id,
RTC_BUFFER_TYPE_VERTEX,
t,
RTC_FORMAT_FLOAT3,
sizeof(float) * 3,
num_verts);
BLI_assert(rtc_verts);
if (rtc_verts) {
for (size_t j = 0; j < num_verts; ++j) {
rtc_verts[0] = verts[j].x;
rtc_verts[1] = verts[j].y;
rtc_verts[2] = verts[j].z;
rtc_verts += 3;
}
}
if (update) {
rtcUpdateGeometryBuffer(geom_id, RTC_BUFFER_TYPE_VERTEX, t);
}
}
}
void add_triangles(BvhBuildContext ctx,
int id,
Span<float3> positions,
Span<int> corner_verts,
Span<MLoopTri> looptris)
{
// size_t prim_offset = mesh->prim_offset;
// const Attribute *attr_mP = NULL;
// size_t num_motion_steps = 1;
// if (mesh->has_motion_blur()) {
// attr_mP = mesh->attributes.find(ATTR_STD_MOTION_VERTEX_POSITION);
// if (attr_mP) {
// num_motion_steps = mesh->get_motion_steps();
// }
// }
// assert(num_motion_steps <= RTC_MAX_TIME_STEP_COUNT);
// num_motion_steps = min(num_motion_steps, (size_t)RTC_MAX_TIME_STEP_COUNT);
RTCGeometry geom_id = rtcNewGeometry(ctx.device, RTC_GEOMETRY_TYPE_TRIANGLE);
rtcSetGeometryBuildQuality(geom_id, ctx.build_quality);
// rtcSetGeometryTimeStepCount(geom_id, num_motion_steps);
unsigned *rtc_indices = static_cast<unsigned *>(rtcSetNewGeometryBuffer(
geom_id, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, sizeof(int) * 3, looptris.size()));
BLI_assert(rtc_indices);
// if (!rtc_indices) {
// VLOG_WARNING << "Embree could not create new geometry buffer for mesh " <<
// mesh->name.c_str()
// << ".\n";
// return;
// }
for (size_t j = 0; j < looptris.size(); ++j) {
rtc_indices[0] = corner_verts[looptris[j].tri[0]];
rtc_indices[1] = corner_verts[looptris[j].tri[1]];
rtc_indices[2] = corner_verts[looptris[j].tri[2]];
rtc_indices += 3;
}
set_tri_vertex_buffer(geom_id, positions, false);
// rtcSetGeometryUserData(geom_id, (void *)prim_offset);
// rtcSetGeometryOccludedFilterFunction(geom_id, kernel_embree_filter_occluded_func);
// rtcSetGeometryIntersectFilterFunction(geom_id, kernel_embree_filter_intersection_func);
// rtcSetGeometryMask(geom_id, 1);
rtcCommitGeometry(geom_id);
rtcAttachGeometryByID(ctx.scene, geom_id, id);
rtcReleaseGeometry(geom_id);
}
void add_mesh(BvhBuildContext ctx, int id, const Mesh &mesh)
{
const MLoopTri *looptri = BKE_mesh_runtime_looptri_ensure(&mesh);
int looptri_len = BKE_mesh_runtime_looptri_len(&mesh);
if (looptri_len == 0) {
return;
}
add_triangles(
ctx, id, mesh.vert_positions(), mesh.corner_verts(), Span<MLoopTri>(looptri, looptri_len));
}
} // namespace
void BVHTree::build_single_mesh(const Mesh &mesh)
{
rtc_device = rtcNewDevice("verbose=0");
BLI_assert(rtc_device);
rtcSetDeviceErrorFunction(rtc_device, rtc_error_func, nullptr);
rtcSetDeviceMemoryMonitorFunction(rtc_device, rtc_memory_monitor_func, nullptr);
rtc_scene = rtcNewScene(rtc_device);
const RTCSceneFlags scene_flags = RTC_SCENE_FLAG_ROBUST;
rtcSetSceneFlags(rtc_scene, scene_flags);
RTCBuildQuality build_quality = RTC_BUILD_QUALITY_MEDIUM;
rtcSetSceneBuildQuality(rtc_scene, build_quality);
BvhBuildContext ctx{rtc_device, rtc_scene, build_quality};
add_mesh(ctx, 0, mesh);
rtcSetSceneProgressMonitorFunction(rtc_scene, rtc_progress_func, nullptr);
rtcCommitScene(rtc_scene);
}
bool BVHTree::ray_intersect1(const BVHRay &ray, BVHRayHit &r_hit) const
{
RTCIntersectContext rtc_ctx;
rtcInitIntersectContext(&rtc_ctx);
RTCRayHit rtc_hit;
rtc_hit.ray.org_x = ray.origin.x;
rtc_hit.ray.org_y = ray.origin.y;
rtc_hit.ray.org_z = ray.origin.z;
rtc_hit.ray.dir_x = ray.direction.x;
rtc_hit.ray.dir_y = ray.direction.y;
rtc_hit.ray.dir_z = ray.direction.z;
rtc_hit.ray.tnear = ray.dist_min;
rtc_hit.ray.tfar = ray.dist_max;
rtc_hit.ray.time = ray.time; /* Motion blur time */
rtc_hit.ray.mask = ray.mask;
rtc_hit.hit.geomID = RTC_INVALID_GEOMETRY_ID;
rtc_hit.hit.instID[0] = RTC_INVALID_GEOMETRY_ID;
rtcIntersect1(rtc_scene, &rtc_ctx, &rtc_hit);
if (rtc_hit.hit.geomID == RTC_INVALID_GEOMETRY_ID ||
rtc_hit.hit.primID == RTC_INVALID_GEOMETRY_ID)
{
return false;
}
r_hit.ray.origin = float3(rtc_hit.ray.org_x, rtc_hit.ray.org_y, rtc_hit.ray.org_z);
r_hit.ray.dist_min = rtc_hit.ray.tnear;
r_hit.ray.direction = float3(rtc_hit.ray.dir_x, rtc_hit.ray.dir_y, rtc_hit.ray.dir_z);
r_hit.ray.time = rtc_hit.ray.time;
r_hit.ray.dist_max = rtc_hit.ray.tfar;
r_hit.ray.mask = rtc_hit.ray.mask;
r_hit.ray.id = rtc_hit.ray.id;
r_hit.ray.flags = rtc_hit.ray.flags;
r_hit.hit.normal = float3(rtc_hit.hit.Ng_x, rtc_hit.hit.Ng_y, rtc_hit.hit.Ng_z);
r_hit.hit.uv = float2(rtc_hit.hit.u, rtc_hit.hit.v);
r_hit.hit.primitive_id = rtc_hit.hit.primID;
r_hit.hit.geometry_id = rtc_hit.hit.geomID;
static constexpr int MAX_INSTANCE_ID_COPY = std::min(BVHHit::MAX_INSTANCE_LEVEL,
RTC_MAX_INSTANCE_LEVEL_COUNT);
for (int i = 0; i < MAX_INSTANCE_ID_COPY; ++i) {
r_hit.hit.instance_id[i] = rtc_hit.hit.instID[i];
}
for (int i = MAX_INSTANCE_ID_COPY; i < BVHHit::MAX_INSTANCE_LEVEL; ++i) {
r_hit.hit.instance_id[i] = BVHHit::INVALID_INSTANCE_ID;
}
return true;
}
} // namespace blender::bvh
#else /* WITH_BVH_EMBREE */
namespace blender::bvh {
BVHTree::BVHTree() {}
BVHTree::~BVHTree() {}
void BVHTree::free() {}
void BVHTree::build_single_mesh(const Mesh &mesh)
{
UNUSED_VARS(mesh);
}
bool BVHTree::ray_intersect1(const BVHRay &ray, BVHRayHit &r_hit) const
{
UNUSED_VARS(ray, r_hit);
return false;
}
} // namespace blender::bvh
#endif /* WITH_BVH_EMBREE */

View File

@ -0,0 +1,577 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Copyright 2020 Blender Foundation. */
#include "testing/testing.h"
#include "MEM_guardedalloc.h"
#include "BKE_appdir.h"
#include "BKE_blender.h"
#include "BKE_bvh.hh"
#include "BKE_bvhutils.h"
#include "BKE_callbacks.h"
#include "BKE_global.h"
#include "BKE_idtype.h"
#include "BKE_lib_id.h"
#include "BKE_main.h"
#include "BKE_main_namemap.h"
#include "BKE_mesh.h"
#include "BKE_mesh.hh"
#include "BKE_modifier.h"
#include "BKE_node.h"
#include "DEG_depsgraph.h"
#include "DNA_genfile.h" /* for DNA_sdna_current_init() */
#include "DNA_mesh_types.h"
#include "DNA_meshdata_types.h"
#include "DNA_object_types.h"
#include "DNA_windowmanager_types.h"
#include "BLI_fileops.h"
#include "BLI_math_base.hh"
#include "BLI_math_rotation.hh"
#include "BLI_path_util.h"
#include "BLI_rand.hh"
#include "BLI_string_ref.hh"
#include "BLI_utildefines.h"
#include "IMB_imbuf.h"
#include "PIL_time_utildefines.h"
#include "BLO_writefile.h"
#include "RNA_define.h"
#include "GHOST_Path-api.hh"
#include "CLG_log.h"
#define DO_PERF_TESTS 1
#define WRITE_DEBUG_MESH_FILES 0
namespace blender::bke::tests {
/* Utility class that records a duration when going out of scope. */
struct ScopedTimer {
private:
std::string name_;
double start_time_;
public:
ScopedTimer(StringRef name) : name_(name), start_time_(PIL_check_seconds_timer()) {}
~ScopedTimer()
{
::testing::Test::RecordProperty(name_, std::to_string(elapsed()));
}
double elapsed() const
{
return PIL_check_seconds_timer() - start_time_;
}
};
class BVHPerfTest : public testing::Test {
public:
unsigned int num_rays_ = 1000000;
unsigned int raycast_seed_ = 121212;
static void SetUpTestSuite()
{
BKE_idtype_init();
// CLG_init();
// BKE_callback_global_init();
// RNA_init();
// BKE_node_system_init();
}
static void TearDownTestSuite()
{
// RNA_exit();
// BKE_node_system_exit();
// BKE_callback_global_finalize();
// CLG_exit();
}
void init_ray(RandomNumberGenerator &rng, float3 &r_origin, float3 &r_direction, float &r_length)
{
r_origin = float3(rng.get_float(), rng.get_float(), rng.get_float()) * 200.0f - 100.0f;
r_direction = rng.get_unit_float3();
r_length = rng.get_float() * 200.0f;
}
void test_raycasts_classic(const Mesh &mesh)
{
BVHTreeFromMesh tree_data;
{
ScopedTimer timer("[Classic] BVH Build");
BKE_bvhtree_from_mesh_get(&tree_data, &mesh, BVHTREE_FROM_LOOPTRI, 4);
}
BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&tree_data); });
if (tree_data.tree == nullptr) {
return;
}
{
ScopedTimer timer("[Classic] Raycast");
RandomNumberGenerator rng(raycast_seed_);
int hits = 0;
for (int i = 0; i < num_rays_; ++i) {
float3 ray_origin;
float3 ray_direction;
float ray_length;
init_ray(rng, ray_origin, ray_direction, ray_length);
BVHTreeRayHit hit;
hit.index = -1;
hit.dist = ray_length;
bool is_hit = (BLI_bvhtree_ray_cast(tree_data.tree,
ray_origin,
ray_direction,
0.0f,
&hit,
tree_data.raycast_callback,
&tree_data) != -1);
if (is_hit) {
++hits;
}
}
RecordProperty("[Classic] Hits", hits);
}
}
void test_raycasts_embree(const Mesh &mesh)
{
bvh::BVHTree tree;
{
ScopedTimer timer("[Embree] BVH Build");
tree.build_single_mesh(mesh);
}
{
ScopedTimer timer("[Embree] Raycast 1");
RandomNumberGenerator rng(raycast_seed_);
int hits = 0;
for (int i = 0; i < num_rays_; ++i) {
bvh::BVHRay ray;
init_ray(rng, ray.origin, ray.direction, ray.dist_max);
bvh::BVHRayHit hit;
bool is_hit = tree.ray_intersect1(ray, hit);
if (is_hit) {
++hits;
}
}
RecordProperty("[Embree] Hits", hits);
}
}
void test_raycasts(const Mesh &mesh)
{
test_raycasts_classic(mesh);
test_raycasts_embree(mesh);
}
};
class BVHPerfParamTest : public BVHPerfTest, public testing::WithParamInterface<int> {};
class BVHBlendFileTest : public testing::Test {
private:
std::string temp_library_path_;
public:
static void SetUpTestSuite()
{
/* Minimal code to make loading a blendfile and constructing a depsgraph not crash, copied from
* main() in creator.c. */
CLG_init();
BLI_threadapi_init();
DNA_sdna_current_init();
BKE_blender_globals_init();
BKE_idtype_init();
BKE_appdir_init();
IMB_init();
BKE_modifier_init();
DEG_register_node_types();
RNA_init();
BKE_node_system_init();
BKE_callback_global_init();
// BKE_vfont_builtin_register(datatoc_bfont_pfb, datatoc_bfont_pfb_size);
G.background = true;
G.factory_startup = true;
/* Allocate a dummy window manager. The real window manager will try and load Python scripts
* from the release directory, which it won't be able to find. */
ASSERT_EQ(G.main->wm.first, nullptr);
G.main->wm.first = MEM_callocN(sizeof(wmWindowManager), __func__);
}
static void TearDownTestSuite()
{
if (G.main->wm.first != nullptr) {
MEM_freeN(G.main->wm.first);
G.main->wm.first = nullptr;
}
/* Copied from WM_exit_ex() in wm_init_exit.cc, and cherry-picked those lines that match the
* allocation/initialization done in SetUpTestCase(). */
BKE_blender_free();
RNA_exit();
DEG_free_node_types();
GHOST_DisposeSystemPaths();
DNA_sdna_current_free();
BLI_threadapi_exit();
BKE_blender_atexit();
BKE_tempdir_session_purge();
BKE_appdir_exit();
CLG_exit();
}
void SetUp() override
{
temp_library_path_ = "";
}
void TearDown() override
{
if (!temp_library_path_.empty()) {
BLI_delete(temp_library_path_.c_str(), true, true);
temp_library_path_ = "";
}
}
/* Register a temporary path, which will be removed at the end of the test.
* The returned path ends in a slash. */
std::string use_temp_path()
{
BKE_tempdir_init("");
const std::string tempdir = BKE_tempdir_session();
temp_library_path_ = tempdir + "test-temporary-path" + SEP_STR;
return temp_library_path_;
}
std::string create_temp_path()
{
std::string path = use_temp_path();
BLI_dir_create_recursive(path.c_str());
return path;
}
/* Register a persistent temporary path. The returned path ends in a slash. */
std::string use_persistent_temp_path()
{
BKE_tempdir_init("");
const std::string tempdir = BKE_tempdir_base();
return tempdir + "test-persistent-path" + SEP_STR;
}
std::string create_persistent_temp_path()
{
std::string path = use_persistent_temp_path();
BLI_dir_create_recursive(path.c_str());
return path;
}
/* Warning: takes ownership of the mesh and frees it at the end! */
void write_debug_mesh_file(Mesh *mesh, const char *filename)
{
Main *bmain = BKE_main_new();
Mesh *main_mesh = BKE_mesh_add(bmain, "BVHTest");
BKE_mesh_nomain_to_mesh(mesh, main_mesh, nullptr);
char filepath[FILE_MAX];
BLI_strncpy(filepath, create_persistent_temp_path().c_str(), sizeof(filepath));
BLI_path_append(filepath, sizeof(filepath), filename);
BlendFileWriteParams write_params{};
write_params.use_save_versions = 0;
BLO_write_file(bmain, filepath, 0, &write_params, nullptr);
std::cout << "BVH mesh file written to " << filepath << std::endl;
BKE_main_free(bmain);
}
};
class MeshGenerator {
public:
int num_triangles_ = 100;
float size_ = 10.0f;
float scale_ = 1.0f;
MeshGenerator(int num_triangles) : num_triangles_(num_triangles)
{
scale_ = size_ / powf(num_triangles_, 0.333333f);
}
};
class UniformAreaTrianglesGenerator : public MeshGenerator {
public:
unsigned int mesh_seed_ = 12345;
float triangle_area() const
{
const float avg_side_length = 0.5f * scale_;
return 0.5f * avg_side_length * avg_side_length;
}
/* Note: don't make this too large, or the rng filter below
* might reject a large proportion of candidate triangles and take a lot of time.
*/
float min_angle_ = DEG2RAD(20.0f);
float min_side_factor = 0.333f;
UniformAreaTrianglesGenerator(int num_triangles) : MeshGenerator(num_triangles) {}
Mesh *create_mesh() const
{
const int num_tris = num_triangles_;
const int num_verts = num_tris * 3;
const int num_corners = num_tris * 3;
Mesh *mesh = BKE_mesh_new_nomain(num_verts, 0, num_tris, num_corners);
MutableSpan<float3> positions = mesh->vert_positions_for_write();
MutableSpan<int> poly_offsets = mesh->poly_offsets_for_write();
MutableSpan<int> corner_verts = mesh->corner_verts_for_write();
RandomNumberGenerator rng(mesh_seed_);
for (int i = 0; i < num_tris; ++i) {
float3 p0 = (float3(rng.get_float(), rng.get_float(), rng.get_float()) - float3(0.5f)) *
size_;
float3 u, v;
float dot_uv;
do {
u = rng.get_unit_float3();
v = rng.get_unit_float3();
dot_uv = math::dot(u, v);
} while (math::abs(dot_uv) >= math::cos(min_angle_));
const float side_scale = math::sqrt(2.0f * triangle_area() /
math::sqrt(1.0f - dot_uv * dot_uv));
const float r = min_side_factor + (1.0f - min_side_factor) * rng.get_float();
const float s = 1.0f / r;
float3 p1 = p0 + side_scale * r * u;
float3 p2 = p0 + side_scale * s * v;
positions[i * 3 + 0] = p0;
positions[i * 3 + 1] = p1;
positions[i * 3 + 2] = p2;
poly_offsets[i] = i * 3;
corner_verts[i * 3 + 0] = i * 3 + 0;
corner_verts[i * 3 + 1] = i * 3 + 1;
corner_verts[i * 3 + 2] = i * 3 + 2;
}
poly_offsets[num_tris] = num_tris * 3;
BKE_mesh_calc_edges(mesh, false, false);
return mesh;
}
};
class DonutCloudGenerator : MeshGenerator {
public:
unsigned int mesh_seed_ = 12345;
int min_segments_ = 4;
int max_segments_ = 128;
DonutCloudGenerator(int num_triangles) : MeshGenerator(num_triangles) {}
void get_donut_params(RandomNumberGenerator &rng,
int &r_major_segments,
int &r_minor_segments,
float &r_major_radius,
float &r_minor_radius,
float3 &r_center,
math::Quaternion &r_rotation) const
{
const float avg_major_radius_ = 4.0f;
r_major_radius = scale_ * avg_major_radius_ * (0.5f + 1.0f * rng.get_float());
r_minor_radius = r_major_radius * (0.25f + 0.5f * rng.get_float());
r_major_segments = min_segments_ + rng.get_int32(max_segments_ - min_segments_ + 1);
r_minor_segments = math::max((int)(r_major_segments * r_minor_radius / r_major_radius),
min_segments_);
r_center = (float3(rng.get_float(), rng.get_float(), rng.get_float()) - float3(0.5f)) * size_;
const float a0 = rng.get_float() * 2.0f * M_PI;
const float a1 = rng.get_float() * 2.0f * M_PI;
const float a2 = rng.get_float() * 2.0f * M_PI;
mul_qt_qtqt((float4 &)r_rotation,
float4(math::sin(a0), 0.0f, 0.0f, math::cos(a0)),
float4(0.0f, math::sin(a1), 0.0f, math::cos(a1)));
mul_qt_qtqt((float4 &)r_rotation,
(float4)r_rotation,
float4(0.0f, 0.0f, math::sin(a2), math::cos(a2)));
}
void generate_donut(MutableSpan<int> poly_offsets,
MutableSpan<int> corner_verts,
MutableSpan<float3> positions,
int verts_start,
int corners_start,
int major_segments,
int minor_segments,
float major_radius,
float minor_radius,
const float3 &center,
const math::Quaternion &rotation) const
{
BLI_assert(poly_offsets.size() == major_segments * minor_segments + 1);
BLI_assert(corner_verts.size() == 4 * major_segments * minor_segments);
BLI_assert(positions.size() == major_segments * minor_segments);
const float major_delta = M_PI * 2.0f / (float)major_segments;
const float minor_delta = M_PI * 2.0f / (float)minor_segments;
for (const int i : IndexRange(major_segments)) {
for (const int j : IndexRange(minor_segments)) {
const int index = i * minor_segments + j;
poly_offsets[index] = corners_start + index * 4;
const int vert00 = verts_start + index;
const int vert10 = j + 1 < minor_segments ? vert00 + 1 : vert00 - j;
const int vert01 = i + 1 < major_segments ? vert00 + minor_segments :
vert00 - i * minor_segments;
const int vert11 = j + 1 < minor_segments ? vert01 + 1 : vert01 - j;
// BLI_assert(vert00 < poly_start + polys.size());
// BLI_assert(vert10 < poly_start + polys.size());
// BLI_assert(vert01 < poly_start + polys.size());
// BLI_assert(vert11 < poly_start + polys.size());
corner_verts[index * 4 + 0] = vert00;
corner_verts[index * 4 + 1] = vert10;
corner_verts[index * 4 + 2] = vert11;
corner_verts[index * 4 + 3] = vert01;
const float major_angle = i * major_delta;
const float minor_angle = j * minor_delta;
const float3 minor_pos = float3(math::cos(minor_angle), 0.0f, math::sin(minor_angle)) *
minor_radius +
float3(major_radius, 0.0f, 0.0f);
const float3 major_pos(minor_pos.x * math::cos(major_angle),
minor_pos.x * math::sin(major_angle),
minor_pos.z);
float3 pos = major_pos;
mul_qt_v3((float4)rotation, pos);
pos += center;
positions[index] = pos;
}
}
poly_offsets[major_segments * minor_segments] = corners_start +
major_segments * minor_segments * 4;
}
Mesh *create_mesh() const
{
/* Dry run for counting triangles */
RandomNumberGenerator rng(mesh_seed_);
int num_donuts = 0;
int num_polys = 0;
const int max_polys = num_triangles_ / 2;
while (true) {
float r_maj, r_min;
int seg_maj, seg_min;
float3 center;
math::Quaternion rotation;
get_donut_params(rng, seg_maj, seg_min, r_maj, r_min, center, rotation);
const int donut_polys = seg_min * seg_maj;
if (num_polys + donut_polys >= max_polys) {
break;
}
++num_donuts;
num_polys += donut_polys;
}
/* Torus has same number of verts as polys, 4 loops per poly */
Mesh *mesh = BKE_mesh_new_nomain(num_polys, 0, num_polys, num_polys * 4);
MutableSpan<float3> positions = mesh->vert_positions_for_write();
MutableSpan<int> poly_offsets = mesh->poly_offsets_for_write();
MutableSpan<int> corner_verts = mesh->corner_verts_for_write();
/* Reset RNG */
rng.seed(mesh_seed_);
int polys_start = 0;
int corners_start = 0;
int verts_start = 0;
for (const int idonut : IndexRange(num_donuts)) {
float r_maj, r_min;
int seg_maj, seg_min;
float3 center;
math::Quaternion rotation;
get_donut_params(rng, seg_maj, seg_min, r_maj, r_min, center, rotation);
const int donut_polys = seg_min * seg_maj;
generate_donut(poly_offsets.slice(polys_start, donut_polys + 1),
corner_verts.slice(corners_start, donut_polys * 4),
positions.slice(verts_start, donut_polys),
verts_start,
corners_start,
seg_maj,
seg_min,
r_maj,
r_min,
center,
rotation);
polys_start += donut_polys;
corners_start += donut_polys * 4;
verts_start += donut_polys;
}
BKE_mesh_calc_edges(mesh, false, false);
// BKE_mesh_validate(mesh, true, true);
return mesh;
}
};
#if DO_PERF_TESTS
TEST_P(BVHPerfParamTest, raycast_uniform_area_triangles)
{
UniformAreaTrianglesGenerator gen(GetParam());
Mesh *mesh = gen.create_mesh();
test_raycasts(*mesh);
BKE_id_free(nullptr, mesh);
}
TEST_P(BVHPerfParamTest, raycast_donut_cloud)
{
DonutCloudGenerator gen(GetParam());
Mesh *mesh = gen.create_mesh();
test_raycasts(*mesh);
BKE_id_free(nullptr, mesh);
}
INSTANTIATE_TEST_SUITE_P(
MeshSizeTests,
BVHPerfParamTest,
testing::Values(
100000, 200000, 300000, 400000, 500000, 1000000, 2000000, 3000000, 4000000, 5000000));
#endif
#if WRITE_DEBUG_MESH_FILES
TEST_F(BVHBlendFileTest, write_mesh_debug_files)
{
{
UniformAreaTrianglesGenerator gen(10000);
Mesh *mesh = gen.create_mesh();
write_debug_mesh_file(mesh, "triangle_soup.blend");
}
{
DonutCloudGenerator gen(1000000);
Mesh *mesh = gen.create_mesh();
write_debug_mesh_file(mesh, "donut_cloud.blend");
}
}
#endif
} // namespace blender::bke::tests

View File

@ -136,6 +136,8 @@ static void mesh_copy_data(Main *bmain, ID *id_dst, const ID *id_src, const int
mesh_dst->runtime->looptris_cache = mesh_src->runtime->looptris_cache;
mesh_dst->runtime->looptri_polys_cache = mesh_src->runtime->looptri_polys_cache;
mesh_dst->runtime->bvh_embree_cache = mesh_src->runtime->bvh_embree_cache;
/* Only do tessface if we have no polys. */
const bool do_tessface = ((mesh_src->totface != 0) && (mesh_src->totpoly == 0));

View File

@ -17,6 +17,7 @@
#include "BLI_task.hh"
#include "BLI_timeit.hh"
#include "BKE_bvh.hh"
#include "BKE_bvhutils.h"
#include "BKE_editmesh_cache.h"
#include "BKE_lib_id.h"
@ -76,6 +77,7 @@ static void free_bvh_cache(MeshRuntime &mesh_runtime)
bvhcache_free(mesh_runtime.bvh_cache);
mesh_runtime.bvh_cache = nullptr;
}
mesh_runtime.bvh_embree_cache.tag_dirty();
}
static void reset_normals(MeshRuntime &mesh_runtime)
@ -221,6 +223,15 @@ blender::Span<MLoopTri> Mesh::looptris() const
return this->runtime->looptris_cache.data();
}
const blender::bvh::BVHTree &Mesh::bvh_tree() const
{
this->runtime->bvh_embree_cache.ensure([&](blender::bvh::BVHTree &r_data) {
r_data.build_single_mesh(*this);
});
return this->runtime->bvh_embree_cache.data();
}
blender::Span<int> Mesh::looptri_polys() const
{
using namespace blender;

View File

@ -30,6 +30,9 @@ class MutableAttributeAccessor;
struct LooseVertCache;
struct LooseEdgeCache;
} // namespace bke
namespace bvh {
class BVHTree;
} // namespace bvh
} // namespace blender
using MeshRuntimeHandle = blender::bke::MeshRuntime;
#else
@ -343,6 +346,8 @@ typedef struct Mesh {
* surrounding each vertex and the normalized position for loose vertices.
*/
blender::Span<blender::float3> vert_normals() const;
const blender::bvh::BVHTree &bvh_tree() const;
#endif
} Mesh;

View File

@ -390,7 +390,9 @@ DefNode(GeometryNode, GEO_NODE_POINTS_TO_VOLUME, def_geo_points_to_volume, "POIN
DefNode(GeometryNode, GEO_NODE_POINTS, 0, "POINTS", Points, "Points", "Generate a point cloud with positions and radii defined by fields")
DefNode(GeometryNode, GEO_NODE_PROXIMITY, def_geo_proximity, "PROXIMITY", Proximity, "Geometry Proximity", "Compute the closest location on the target geometry")
DefNode(GeometryNode, GEO_NODE_RAYCAST, def_geo_raycast, "RAYCAST", Raycast, "Raycast", "Cast rays from the context geometry onto a target geometry, and retrieve information from each hit point")
DefNode(GeometryNode, GEO_NODE_RAYCAST_EMBREE, def_geo_raycast, "RAYCAST_EMBREE", RaycastEmbree, "Raycast (Embree)", "Cast rays from the context geometry onto a target geometry, and retrieve information from each hit point")
DefNode(GeometryNode, GEO_NODE_REALIZE_INSTANCES, 0, "REALIZE_INSTANCES", RealizeInstances, "Realize Instances", "Convert instances into real geometry data")
DefNode(GeometryNode, GEO_NODE_REMOVE_ATTRIBUTE, 0, "REMOVE_ATTRIBUTE", RemoveAttribute, "Remove Named Attribute", "Delete an attribute with a specified name from a geometry. Typically used to optimize performance")
DefNode(GeometryNode, GEO_NODE_REPLACE_MATERIAL, 0, "REPLACE_MATERIAL", ReplaceMaterial, "Replace Material", "Swap one material with another")
DefNode(GeometryNode, GEO_NODE_RESAMPLE_CURVE, def_geo_curve_resample, "RESAMPLE_CURVE", ResampleCurve, "Resample Curve", "Generate a poly spline for each input spline")

View File

@ -130,6 +130,7 @@ void register_geometry_nodes()
register_node_type_geo_points();
register_node_type_geo_proximity();
register_node_type_geo_raycast();
register_node_type_geo_raycast_embree();
register_node_type_geo_realize_instances();
register_node_type_geo_remove_attribute();
register_node_type_geo_rotate_instances();

View File

@ -127,6 +127,7 @@ void register_node_type_geo_points_to_volume();
void register_node_type_geo_points();
void register_node_type_geo_proximity();
void register_node_type_geo_raycast();
void register_node_type_geo_raycast_embree();
void register_node_type_geo_realize_instances();
void register_node_type_geo_remove_attribute();
void register_node_type_geo_rotate_instances();

View File

@ -1,11 +1,7 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_index_mask_ops.hh"
#include "DNA_mesh_types.h"
#include "BKE_attribute_math.hh"
#include "BKE_bvhutils.h"
#include "BKE_mesh_sample.hh"
#include "UI_interface.h"
@ -15,6 +11,9 @@
#include "node_geometry_util.hh"
#include "BKE_bvh.hh"
#include "BKE_bvhutils.h"
namespace blender::nodes::node_geo_raycast_cc {
using namespace blender::bke::mesh_surface_sample;
@ -116,7 +115,10 @@ static void node_gather_link_searches(GatherLinkSearchOpParams &params)
}
}
static void raycast_to_mesh(IndexMask mask,
enum BVHType { Classic, Embree };
static void raycast_to_mesh(BVHType bvh_type,
IndexMask mask,
const Mesh &mesh,
const VArray<float3> &ray_origins,
const VArray<float3> &ray_directions,
@ -127,75 +129,131 @@ static void raycast_to_mesh(IndexMask mask,
const MutableSpan<float3> r_hit_normals,
const MutableSpan<float> r_hit_distances)
{
BVHTreeFromMesh tree_data;
BKE_bvhtree_from_mesh_get(&tree_data, &mesh, BVHTREE_FROM_LOOPTRI, 4);
BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&tree_data); });
switch (bvh_type) {
case BVHType::Classic: {
BVHTreeFromMesh tree_data;
BKE_bvhtree_from_mesh_get(&tree_data, &mesh, BVHTREE_FROM_LOOPTRI, 4);
BLI_SCOPED_DEFER([&]() { free_bvhtree_from_mesh(&tree_data); });
if (tree_data.tree == nullptr) {
return;
}
/* We shouldn't be rebuilding the BVH tree when calling this function in parallel. */
BLI_assert(tree_data.cached);
if (tree_data.tree == nullptr) {
return;
}
/* We shouldn't be rebuilding the BVH tree when calling this function in parallel. */
BLI_assert(tree_data.cached);
for (const int i : mask) {
const float ray_length = ray_lengths[i];
const float3 ray_origin = ray_origins[i];
const float3 ray_direction = ray_directions[i];
for (const int i : mask) {
const float ray_length = ray_lengths[i];
const float3 ray_origin = ray_origins[i];
const float3 ray_direction = ray_directions[i];
BVHTreeRayHit hit;
hit.index = -1;
hit.dist = ray_length;
if (BLI_bvhtree_ray_cast(tree_data.tree,
ray_origin,
ray_direction,
0.0f,
&hit,
tree_data.raycast_callback,
&tree_data) != -1)
{
if (!r_hit.is_empty()) {
r_hit[i] = hit.index >= 0;
BVHTreeRayHit hit;
hit.index = -1;
hit.dist = ray_length;
if (BLI_bvhtree_ray_cast(tree_data.tree,
ray_origin,
ray_direction,
0.0f,
&hit,
tree_data.raycast_callback,
&tree_data) != -1)
{
if (!r_hit.is_empty()) {
r_hit[i] = hit.index >= 0;
}
if (!r_hit_indices.is_empty()) {
/* The caller must be able to handle invalid indices anyway, so don't clamp this value.
*/
r_hit_indices[i] = hit.index;
}
if (!r_hit_positions.is_empty()) {
r_hit_positions[i] = hit.co;
}
if (!r_hit_normals.is_empty()) {
r_hit_normals[i] = hit.no;
}
if (!r_hit_distances.is_empty()) {
r_hit_distances[i] = hit.dist;
}
}
else {
if (!r_hit.is_empty()) {
r_hit[i] = false;
}
if (!r_hit_indices.is_empty()) {
r_hit_indices[i] = -1;
}
if (!r_hit_positions.is_empty()) {
r_hit_positions[i] = float3(0.0f, 0.0f, 0.0f);
}
if (!r_hit_normals.is_empty()) {
r_hit_normals[i] = float3(0.0f, 0.0f, 0.0f);
}
if (!r_hit_distances.is_empty()) {
r_hit_distances[i] = ray_length;
}
}
}
if (!r_hit_indices.is_empty()) {
/* The caller must be able to handle invalid indices anyway, so don't clamp this value. */
r_hit_indices[i] = hit.index;
} break;
case BVHType::Embree: {
const bvh::BVHTree &tree = mesh.bvh_tree();
for (const int i : mask) {
bvh::BVHRay ray;
ray.origin = ray_origins[i];
ray.direction = ray_directions[i];
ray.dist_max = ray_lengths[i];
bvh::BVHRayHit hit;
if (tree.ray_intersect1(ray, hit)) {
if (!r_hit.is_empty()) {
r_hit[i] = true;
}
if (!r_hit_indices.is_empty()) {
/* The caller must be able to handle invalid indices anyway, so don't clamp this value.
*/
r_hit_indices[i] = hit.hit.primitive_id;
}
if (!r_hit_positions.is_empty()) {
r_hit_positions[i] = hit.ray.origin + hit.ray.direction * hit.ray.dist_max;
}
if (!r_hit_normals.is_empty()) {
r_hit_normals[i] = hit.hit.normal;
}
if (!r_hit_distances.is_empty()) {
r_hit_distances[i] = math::length(hit.ray.direction * hit.ray.dist_max);
}
}
else {
if (!r_hit.is_empty()) {
r_hit[i] = false;
}
if (!r_hit_indices.is_empty()) {
r_hit_indices[i] = -1;
}
if (!r_hit_positions.is_empty()) {
r_hit_positions[i] = float3(0.0f, 0.0f, 0.0f);
}
if (!r_hit_normals.is_empty()) {
r_hit_normals[i] = float3(0.0f, 0.0f, 0.0f);
}
if (!r_hit_distances.is_empty()) {
r_hit_distances[i] = ray_lengths[i];
}
}
}
if (!r_hit_positions.is_empty()) {
r_hit_positions[i] = hit.co;
}
if (!r_hit_normals.is_empty()) {
r_hit_normals[i] = hit.no;
}
if (!r_hit_distances.is_empty()) {
r_hit_distances[i] = hit.dist;
}
}
else {
if (!r_hit.is_empty()) {
r_hit[i] = false;
}
if (!r_hit_indices.is_empty()) {
r_hit_indices[i] = -1;
}
if (!r_hit_positions.is_empty()) {
r_hit_positions[i] = float3(0.0f, 0.0f, 0.0f);
}
if (!r_hit_normals.is_empty()) {
r_hit_normals[i] = float3(0.0f, 0.0f, 0.0f);
}
if (!r_hit_distances.is_empty()) {
r_hit_distances[i] = ray_length;
}
}
} break;
}
}
class RaycastFunction : public mf::MultiFunction {
private:
BVHType bvh_type_;
GeometrySet target_;
public:
RaycastFunction(GeometrySet target) : target_(std::move(target))
RaycastFunction(BVHType bvh_type, GeometrySet target)
: bvh_type_(bvh_type), target_(std::move(target))
{
target_.ensure_owns_direct_data();
static const mf::Signature signature = []() {
@ -219,7 +277,8 @@ class RaycastFunction : public mf::MultiFunction {
BLI_assert(target_.has_mesh());
const Mesh &mesh = *target_.get_mesh_for_read();
raycast_to_mesh(mask,
raycast_to_mesh(bvh_type_,
mask,
mesh,
params.readonly_single_input<float3>(0, "Source Position"),
params.readonly_single_input<float3>(1, "Ray Direction"),
@ -294,7 +353,7 @@ static void output_attribute_field(GeoNodeExecParams &params, GField field)
}
}
static void node_geo_exec(GeoNodeExecParams params)
static void node_geo_exec(GeoNodeExecParams params, BVHType bvh_type)
{
GeometrySet target = params.extract_input<GeometrySet>("Target Geometry");
const NodeGeometryRaycast &storage = node_storage(params.node());
@ -324,7 +383,7 @@ static void node_geo_exec(GeoNodeExecParams params)
auto direction_op = FieldOperation::Create(
normalize_fn, {params.extract_input<Field<float3>>("Ray Direction")});
auto op = FieldOperation::Create(std::make_unique<RaycastFunction>(target),
auto op = FieldOperation::Create(std::make_unique<RaycastFunction>(bvh_type, target),
{params.extract_input<Field<float3>>("Source Position"),
Field<float3>(direction_op),
params.extract_input<Field<float>>("Ray Length")});
@ -357,6 +416,16 @@ static void node_geo_exec(GeoNodeExecParams params)
}
}
static void node_geo_exec_classic(GeoNodeExecParams params)
{
node_geo_exec(params, BVHType::Classic);
}
static void node_geo_exec_embree(GeoNodeExecParams params)
{
node_geo_exec(params, BVHType::Embree);
}
} // namespace blender::nodes::node_geo_raycast_cc
void register_node_type_geo_raycast()
@ -372,7 +441,26 @@ void register_node_type_geo_raycast()
node_type_storage(
&ntype, "NodeGeometryRaycast", node_free_standard_storage, node_copy_standard_storage);
ntype.declare = file_ns::node_declare;
ntype.geometry_node_execute = file_ns::node_geo_exec;
ntype.geometry_node_execute = file_ns::node_geo_exec_classic;
ntype.draw_buttons = file_ns::node_layout;
ntype.gather_link_search_ops = file_ns::node_gather_link_searches;
nodeRegisterType(&ntype);
}
void register_node_type_geo_raycast_embree()
{
namespace file_ns = blender::nodes::node_geo_raycast_cc;
static bNodeType ntype;
geo_node_type_base(&ntype, GEO_NODE_RAYCAST_EMBREE, "Raycast (Embree)", NODE_CLASS_GEOMETRY);
node_type_size_preset(&ntype, blender::bke::eNodeSizePreset::MIDDLE);
ntype.initfunc = file_ns::node_init;
ntype.updatefunc = file_ns::node_update;
node_type_storage(
&ntype, "NodeGeometryRaycast", node_free_standard_storage, node_copy_standard_storage);
ntype.declare = file_ns::node_declare;
ntype.geometry_node_execute = file_ns::node_geo_exec_embree;
ntype.draw_buttons = file_ns::node_layout;
ntype.gather_link_search_ops = file_ns::node_gather_link_searches;
nodeRegisterType(&ntype);