Compare commits
4 Commits
node-group
...
temp-defor
Author | SHA1 | Date | |
---|---|---|---|
b39cd9a849 | |||
e142387b2f | |||
349682e5bf | |||
01a223fe0c |
@@ -411,6 +411,8 @@ class DATA_PT_shape_keys(MeshButtonsPanel, Panel):
|
||||
row.active = enable_edit_value
|
||||
row.prop(key, "eval_time")
|
||||
|
||||
layout.prop(ob, "add_rest_position_attribute")
|
||||
|
||||
|
||||
class DATA_PT_uv_texture(MeshButtonsPanel, Panel):
|
||||
bl_label = "UV Maps"
|
||||
|
@@ -73,6 +73,7 @@ def curve_node_items(context):
|
||||
yield NodeItem("GeometryNodeCurveLength")
|
||||
yield NodeItem("GeometryNodeCurveToMesh")
|
||||
yield NodeItem("GeometryNodeCurveToPoints")
|
||||
yield NodeItem("GeometryNodeDeformCurvesWithSurface")
|
||||
yield NodeItem("GeometryNodeFillCurve")
|
||||
yield NodeItem("GeometryNodeFilletCurve")
|
||||
yield NodeItem("GeometryNodeResampleCurve")
|
||||
|
@@ -710,7 +710,8 @@ void BKE_mesh_calc_normals_split(struct Mesh *mesh);
|
||||
* to split geometry along sharp edges.
|
||||
*/
|
||||
void BKE_mesh_calc_normals_split_ex(struct Mesh *mesh,
|
||||
struct MLoopNorSpaceArray *r_lnors_spacearr);
|
||||
struct MLoopNorSpaceArray *r_lnors_spacearr,
|
||||
float (*r_corner_normals)[3]);
|
||||
|
||||
/**
|
||||
* Higher level functions hiding most of the code needed around call to
|
||||
|
@@ -1501,6 +1501,7 @@ struct TexResult;
|
||||
#define GEO_NODE_MESH_TO_VOLUME 1164
|
||||
#define GEO_NODE_UV_UNWRAP 1165
|
||||
#define GEO_NODE_UV_PACK_ISLANDS 1166
|
||||
#define GEO_NODE_DEFORM_CURVES_ON_SURFACE 1167
|
||||
|
||||
/** \} */
|
||||
|
||||
|
@@ -66,6 +66,9 @@
|
||||
# include "DNA_userdef_types.h"
|
||||
#endif
|
||||
|
||||
using blender::float3;
|
||||
using blender::IndexRange;
|
||||
|
||||
/* very slow! enable for testing only! */
|
||||
//#define USE_MODIFIER_VALIDATE
|
||||
|
||||
@@ -814,6 +817,25 @@ static void mesh_calc_modifiers(struct Depsgraph *depsgraph,
|
||||
/* Clear errors before evaluation. */
|
||||
BKE_modifiers_clear_errors(ob);
|
||||
|
||||
if (ob->modifier_flag & OB_MODIFIER_FLAG_ADD_REST_POSITION) {
|
||||
if (mesh_final == nullptr) {
|
||||
mesh_final = BKE_mesh_copy_for_eval(mesh_input, true);
|
||||
ASSERT_IS_VALID_MESH(mesh_final);
|
||||
}
|
||||
float3 *rest_positions = static_cast<float3 *>(CustomData_add_layer_named(&mesh_final->vdata,
|
||||
CD_PROP_FLOAT3,
|
||||
CD_DEFAULT,
|
||||
nullptr,
|
||||
mesh_final->totvert,
|
||||
"rest_position"));
|
||||
blender::threading::parallel_for(
|
||||
IndexRange(mesh_final->totvert), 1024, [&](const IndexRange range) {
|
||||
for (const int i : range) {
|
||||
rest_positions[i] = mesh_final->mvert[i].co;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Apply all leading deform modifiers. */
|
||||
if (use_deform) {
|
||||
for (; md; md = md->next, md_datamask = md_datamask->next) {
|
||||
|
@@ -1922,9 +1922,25 @@ void BKE_mesh_vert_coords_apply_with_mat4(Mesh *mesh,
|
||||
BKE_mesh_tag_coords_changed(mesh);
|
||||
}
|
||||
|
||||
void BKE_mesh_calc_normals_split_ex(Mesh *mesh, MLoopNorSpaceArray *r_lnors_spacearr)
|
||||
static float (*ensure_corner_normal_layer(Mesh &mesh))[3]
|
||||
{
|
||||
float(*r_loopnors)[3];
|
||||
if (CustomData_has_layer(&mesh.ldata, CD_NORMAL)) {
|
||||
r_loopnors = (float(*)[3])CustomData_get_layer(&mesh.ldata, CD_NORMAL);
|
||||
memset(r_loopnors, 0, sizeof(float[3]) * mesh.totloop);
|
||||
}
|
||||
else {
|
||||
r_loopnors = (float(*)[3])CustomData_add_layer(
|
||||
&mesh.ldata, CD_NORMAL, CD_CALLOC, nullptr, mesh.totloop);
|
||||
CustomData_set_layer_flag(&mesh.ldata, CD_NORMAL, CD_FLAG_TEMPORARY);
|
||||
}
|
||||
return r_loopnors;
|
||||
}
|
||||
|
||||
void BKE_mesh_calc_normals_split_ex(Mesh *mesh,
|
||||
MLoopNorSpaceArray *r_lnors_spacearr,
|
||||
float (*r_corner_normals)[3])
|
||||
{
|
||||
short(*clnors)[2] = nullptr;
|
||||
|
||||
/* Note that we enforce computing clnors when the clnor space array is requested by caller here.
|
||||
@@ -1934,16 +1950,6 @@ void BKE_mesh_calc_normals_split_ex(Mesh *mesh, MLoopNorSpaceArray *r_lnors_spac
|
||||
((mesh->flag & ME_AUTOSMOOTH) != 0);
|
||||
const float split_angle = (mesh->flag & ME_AUTOSMOOTH) != 0 ? mesh->smoothresh : (float)M_PI;
|
||||
|
||||
if (CustomData_has_layer(&mesh->ldata, CD_NORMAL)) {
|
||||
r_loopnors = (float(*)[3])CustomData_get_layer(&mesh->ldata, CD_NORMAL);
|
||||
memset(r_loopnors, 0, sizeof(float[3]) * mesh->totloop);
|
||||
}
|
||||
else {
|
||||
r_loopnors = (float(*)[3])CustomData_add_layer(
|
||||
&mesh->ldata, CD_NORMAL, CD_CALLOC, nullptr, mesh->totloop);
|
||||
CustomData_set_layer_flag(&mesh->ldata, CD_NORMAL, CD_FLAG_TEMPORARY);
|
||||
}
|
||||
|
||||
/* may be nullptr */
|
||||
clnors = (short(*)[2])CustomData_get_layer(&mesh->ldata, CD_CUSTOMLOOPNORMAL);
|
||||
|
||||
@@ -1953,7 +1959,7 @@ void BKE_mesh_calc_normals_split_ex(Mesh *mesh, MLoopNorSpaceArray *r_lnors_spac
|
||||
mesh->medge,
|
||||
mesh->totedge,
|
||||
mesh->mloop,
|
||||
r_loopnors,
|
||||
r_corner_normals,
|
||||
mesh->totloop,
|
||||
mesh->mpoly,
|
||||
BKE_mesh_poly_normals_ensure(mesh),
|
||||
@@ -1969,7 +1975,7 @@ void BKE_mesh_calc_normals_split_ex(Mesh *mesh, MLoopNorSpaceArray *r_lnors_spac
|
||||
|
||||
void BKE_mesh_calc_normals_split(Mesh *mesh)
|
||||
{
|
||||
BKE_mesh_calc_normals_split_ex(mesh, nullptr);
|
||||
BKE_mesh_calc_normals_split_ex(mesh, nullptr, ensure_corner_normal_layer(*mesh));
|
||||
}
|
||||
|
||||
/* Split faces helper functions. */
|
||||
@@ -2188,7 +2194,8 @@ void BKE_mesh_split_faces(Mesh *mesh, bool free_loop_normals)
|
||||
|
||||
MLoopNorSpaceArray lnors_spacearr = {nullptr};
|
||||
/* Compute loop normals and loop normal spaces (a.k.a. smooth fans of faces around vertices). */
|
||||
BKE_mesh_calc_normals_split_ex(mesh, &lnors_spacearr);
|
||||
|
||||
BKE_mesh_calc_normals_split_ex(mesh, &lnors_spacearr, ensure_corner_normal_layer(*mesh));
|
||||
/* Stealing memarena from loop normals space array. */
|
||||
MemArena *memarena = lnors_spacearr.mem;
|
||||
|
||||
|
@@ -4749,6 +4749,7 @@ static void registerGeometryNodes()
|
||||
register_node_type_geo_curve_to_mesh();
|
||||
register_node_type_geo_curve_to_points();
|
||||
register_node_type_geo_curve_trim();
|
||||
register_node_type_geo_deform_curves_on_surface();
|
||||
register_node_type_geo_delete_geometry();
|
||||
register_node_type_geo_duplicate_elements();
|
||||
register_node_type_geo_distribute_points_on_faces();
|
||||
|
@@ -6,12 +6,96 @@
|
||||
|
||||
#include "BLI_rand.hh"
|
||||
|
||||
#include "BKE_context.h"
|
||||
#include "BKE_curves.hh"
|
||||
#include "BKE_node.h"
|
||||
#include "BKE_node_runtime.hh"
|
||||
|
||||
#include "ED_curves.h"
|
||||
#include "ED_node.h"
|
||||
#include "ED_object.h"
|
||||
|
||||
#include "DNA_modifier_types.h"
|
||||
#include "DNA_node_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
namespace blender::ed::curves {
|
||||
|
||||
static bool has_surface_deformation_node(const bNodeTree &ntree)
|
||||
{
|
||||
LISTBASE_FOREACH (const bNode *, node, &ntree.nodes) {
|
||||
if (node->type == GEO_NODE_DEFORM_CURVES_ON_SURFACE) {
|
||||
return true;
|
||||
}
|
||||
if (node->type == NODE_GROUP) {
|
||||
if (node->id != nullptr) {
|
||||
if (has_surface_deformation_node(*reinterpret_cast<const bNodeTree *>(node->id))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool has_surface_deformation_node(const Object &curves_ob)
|
||||
{
|
||||
LISTBASE_FOREACH (const ModifierData *, md, &curves_ob.modifiers) {
|
||||
if (md->type != eModifierType_Nodes) {
|
||||
continue;
|
||||
}
|
||||
const NodesModifierData *nmd = reinterpret_cast<const NodesModifierData *>(md);
|
||||
if (nmd->node_group == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (has_surface_deformation_node(*nmd->node_group)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ensure_surface_deformation_node_exists(bContext &C, Object &curves_ob)
|
||||
{
|
||||
if (has_surface_deformation_node(curves_ob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Main *bmain = CTX_data_main(&C);
|
||||
Scene *scene = CTX_data_scene(&C);
|
||||
|
||||
ModifierData *md = ED_object_modifier_add(
|
||||
nullptr, bmain, scene, &curves_ob, "Surface Deform", eModifierType_Nodes);
|
||||
NodesModifierData &nmd = *reinterpret_cast<NodesModifierData *>(md);
|
||||
nmd.node_group = ntreeAddTree(bmain, "Surface Deform", "GeometryNodeTree");
|
||||
|
||||
bNodeTree *ntree = nmd.node_group;
|
||||
ntreeAddSocketInterface(ntree, SOCK_IN, "NodeSocketGeometry", "Geometry");
|
||||
ntreeAddSocketInterface(ntree, SOCK_OUT, "NodeSocketGeometry", "Geometry");
|
||||
bNode *group_input = nodeAddStaticNode(&C, ntree, NODE_GROUP_INPUT);
|
||||
bNode *group_output = nodeAddStaticNode(&C, ntree, NODE_GROUP_OUTPUT);
|
||||
bNode *deform_node = nodeAddStaticNode(&C, ntree, GEO_NODE_DEFORM_CURVES_ON_SURFACE);
|
||||
|
||||
ED_node_tree_propagate_change(&C, bmain, nmd.node_group);
|
||||
|
||||
nodeAddLink(ntree,
|
||||
group_input,
|
||||
static_cast<bNodeSocket *>(group_input->outputs.first),
|
||||
deform_node,
|
||||
nodeFindSocket(deform_node, SOCK_IN, "Curves"));
|
||||
nodeAddLink(ntree,
|
||||
deform_node,
|
||||
nodeFindSocket(deform_node, SOCK_OUT, "Curves"),
|
||||
group_output,
|
||||
static_cast<bNodeSocket *>(group_output->inputs.first));
|
||||
|
||||
group_input->locx = -200;
|
||||
group_output->locx = 200;
|
||||
deform_node->locx = 0;
|
||||
|
||||
ED_node_tree_propagate_change(&C, bmain, nmd.node_group);
|
||||
}
|
||||
|
||||
bke::CurvesGeometry primitive_random_sphere(const int curves_size, const int points_per_curve)
|
||||
{
|
||||
bke::CurvesGeometry curves(points_per_curve * curves_size, curves_size);
|
||||
|
@@ -944,6 +944,88 @@ static void SCULPT_CURVES_OT_select_all(wmOperatorType *ot)
|
||||
WM_operator_properties_select_all(ot);
|
||||
}
|
||||
|
||||
namespace surface_set {
|
||||
|
||||
static bool surface_set_poll(bContext *C)
|
||||
{
|
||||
const Object *object = CTX_data_active_object(C);
|
||||
if (object == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (object->type != OB_MESH) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int surface_set_exec(bContext *C, wmOperator *op)
|
||||
{
|
||||
Main *bmain = CTX_data_main(C);
|
||||
Scene *scene = CTX_data_scene(C);
|
||||
|
||||
Object &new_surface_ob = *CTX_data_active_object(C);
|
||||
|
||||
Mesh &new_surface_mesh = *static_cast<Mesh *>(new_surface_ob.data);
|
||||
const char *new_uv_map_name = CustomData_get_active_layer_name(&new_surface_mesh.ldata,
|
||||
CD_MLOOPUV);
|
||||
|
||||
CTX_DATA_BEGIN (C, Object *, selected_ob, selected_objects) {
|
||||
if (selected_ob->type != OB_CURVES) {
|
||||
continue;
|
||||
}
|
||||
Object &curves_ob = *selected_ob;
|
||||
Curves &curves_id = *static_cast<Curves *>(curves_ob.data);
|
||||
|
||||
MEM_SAFE_FREE(curves_id.surface_uv_map);
|
||||
if (new_uv_map_name != nullptr) {
|
||||
curves_id.surface_uv_map = BLI_strdup(new_uv_map_name);
|
||||
}
|
||||
|
||||
bool missing_uvs;
|
||||
bool invalid_uvs;
|
||||
snap_curves_to_surface::snap_curves_to_surface_exec_object(
|
||||
curves_ob,
|
||||
new_surface_ob,
|
||||
snap_curves_to_surface::AttachMode::Nearest,
|
||||
&invalid_uvs,
|
||||
&missing_uvs);
|
||||
|
||||
/* Add deformation modifier if necessary. */
|
||||
blender::ed::curves::ensure_surface_deformation_node_exists(*C, curves_ob);
|
||||
|
||||
/* TODO: Not sure if I have to do id reference counting here. */
|
||||
curves_id.surface = &new_surface_ob;
|
||||
ED_object_parent_set(
|
||||
op->reports, C, scene, &curves_ob, &new_surface_ob, PAR_OBJECT, false, true, nullptr);
|
||||
|
||||
DEG_id_tag_update(&curves_ob.id, ID_RECALC_TRANSFORM);
|
||||
WM_event_add_notifier(C, NC_GEOM | ND_DATA, &curves_id);
|
||||
|
||||
/* Required for deformation. */
|
||||
new_surface_ob.modifier_flag |= OB_MODIFIER_FLAG_ADD_REST_POSITION;
|
||||
DEG_id_tag_update(&new_surface_ob.id, ID_RECALC_GEOMETRY);
|
||||
}
|
||||
CTX_DATA_END;
|
||||
|
||||
DEG_relations_tag_update(bmain);
|
||||
|
||||
return OPERATOR_FINISHED;
|
||||
}
|
||||
|
||||
} // namespace surface_set
|
||||
|
||||
static void CURVES_OT_surface_set(wmOperatorType *ot)
|
||||
{
|
||||
ot->name = "Set Curves Surface Object";
|
||||
ot->idname = __func__;
|
||||
ot->description = "Use the active object as surface for selected curves objects";
|
||||
|
||||
ot->exec = surface_set::surface_set_exec;
|
||||
ot->poll = surface_set::surface_set_poll;
|
||||
|
||||
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
|
||||
}
|
||||
|
||||
} // namespace blender::ed::curves
|
||||
|
||||
void ED_operatortypes_curves()
|
||||
@@ -955,4 +1037,5 @@ void ED_operatortypes_curves()
|
||||
WM_operatortype_append(CURVES_OT_set_selection_domain);
|
||||
WM_operatortype_append(SCULPT_CURVES_OT_select_all);
|
||||
WM_operatortype_append(CURVES_OT_disable_selection);
|
||||
WM_operatortype_append(CURVES_OT_surface_set);
|
||||
}
|
||||
|
@@ -29,6 +29,7 @@ bke::CurvesGeometry primitive_random_sphere(int curves_size, int points_per_curv
|
||||
bool selection_operator_poll(bContext *C);
|
||||
bool has_anything_selected(const Curves &curves_id);
|
||||
VectorSet<Curves *> get_unique_editable_curves(const bContext &C);
|
||||
void ensure_surface_deformation_node_exists(bContext &C, Object &curves_ob);
|
||||
|
||||
} // namespace blender::ed::curves
|
||||
#endif
|
||||
|
@@ -24,6 +24,7 @@
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_mesh_types.h"
|
||||
#include "DNA_meta_types.h"
|
||||
#include "DNA_modifier_types.h"
|
||||
#include "DNA_object_fluidsim_types.h"
|
||||
#include "DNA_object_force_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
@@ -72,6 +73,7 @@
|
||||
#include "BKE_mesh.h"
|
||||
#include "BKE_mesh_runtime.h"
|
||||
#include "BKE_nla.h"
|
||||
#include "BKE_node.h"
|
||||
#include "BKE_object.h"
|
||||
#include "BKE_particle.h"
|
||||
#include "BKE_pointcloud.h"
|
||||
@@ -114,6 +116,10 @@
|
||||
|
||||
#include "object_intern.h"
|
||||
|
||||
using blender::float3;
|
||||
using blender::float4x4;
|
||||
using blender::Vector;
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Local Enum Declarations
|
||||
* \{ */
|
||||
@@ -2071,30 +2077,44 @@ void OBJECT_OT_curves_random_add(wmOperatorType *ot)
|
||||
|
||||
static int object_curves_empty_hair_add_exec(bContext *C, wmOperator *op)
|
||||
{
|
||||
Scene *scene = CTX_data_scene(C);
|
||||
|
||||
ushort local_view_bits;
|
||||
float loc[3], rot[3];
|
||||
blender::float3 loc, rot;
|
||||
if (!ED_object_add_generic_get_opts(
|
||||
C, op, 'Z', loc, rot, nullptr, nullptr, &local_view_bits, nullptr)) {
|
||||
return OPERATOR_CANCELLED;
|
||||
}
|
||||
|
||||
Object *surface_ob = CTX_data_active_object(C);
|
||||
BLI_assert(surface_ob != nullptr);
|
||||
|
||||
Object *object = ED_object_add_type(C, OB_CURVES, nullptr, loc, rot, false, local_view_bits);
|
||||
object->dtx |= OB_DRAWBOUNDOX; /* TODO: remove once there is actual drawing. */
|
||||
Object *curves_ob = ED_object_add_type(C, OB_CURVES, nullptr, loc, rot, false, local_view_bits);
|
||||
curves_ob->dtx |= OB_DRAWBOUNDOX; /* TODO: remove once there is actual drawing. */
|
||||
|
||||
if (surface_ob != nullptr && surface_ob->type == OB_MESH) {
|
||||
Curves *curves_id = static_cast<Curves *>(object->data);
|
||||
curves_id->surface = surface_ob;
|
||||
id_us_plus(&surface_ob->id);
|
||||
/* Set surface object. */
|
||||
Curves *curves_id = static_cast<Curves *>(curves_ob->data);
|
||||
curves_id->surface = surface_ob;
|
||||
id_us_plus(&surface_ob->id);
|
||||
|
||||
Mesh *surface_mesh = static_cast<Mesh *>(surface_ob->data);
|
||||
const char *uv_name = CustomData_get_active_layer_name(&surface_mesh->ldata, CD_MLOOPUV);
|
||||
if (uv_name != nullptr) {
|
||||
curves_id->surface_uv_map = BLI_strdup(uv_name);
|
||||
}
|
||||
/* Parent to surface object. */
|
||||
ED_object_parent_set(
|
||||
op->reports, C, scene, curves_ob, surface_ob, PAR_OBJECT, false, true, nullptr);
|
||||
|
||||
/* Decide which UV map to use for attachment. */
|
||||
Mesh *surface_mesh = static_cast<Mesh *>(surface_ob->data);
|
||||
const char *uv_name = CustomData_get_active_layer_name(&surface_mesh->ldata, CD_MLOOPUV);
|
||||
if (uv_name != nullptr) {
|
||||
curves_id->surface_uv_map = BLI_strdup(uv_name);
|
||||
}
|
||||
|
||||
/* Add deformation modifier. */
|
||||
blender::ed::curves::ensure_surface_deformation_node_exists(*C, *curves_ob);
|
||||
|
||||
/* Make sure the surface object has a rest position attribute which is necessary for
|
||||
* deformations. */
|
||||
surface_ob->modifier_flag |= OB_MODIFIER_FLAG_ADD_REST_POSITION;
|
||||
|
||||
return OPERATOR_FINISHED;
|
||||
}
|
||||
|
||||
|
@@ -951,7 +951,7 @@ static int parent_set_invoke_menu(bContext *C, wmOperatorType *ot)
|
||||
1);
|
||||
|
||||
struct {
|
||||
bool mesh, gpencil;
|
||||
bool mesh, gpencil, curves;
|
||||
} has_children_of_type = {0};
|
||||
|
||||
CTX_DATA_BEGIN (C, Object *, child, selected_editable_objects) {
|
||||
@@ -964,6 +964,9 @@ static int parent_set_invoke_menu(bContext *C, wmOperatorType *ot)
|
||||
if (child->type == OB_GPENCIL) {
|
||||
has_children_of_type.gpencil = true;
|
||||
}
|
||||
if (child->type == OB_CURVES) {
|
||||
has_children_of_type.curves = true;
|
||||
}
|
||||
}
|
||||
CTX_DATA_END;
|
||||
|
||||
@@ -987,6 +990,11 @@ static int parent_set_invoke_menu(bContext *C, wmOperatorType *ot)
|
||||
else if (parent->type == OB_LATTICE) {
|
||||
uiItemEnumO_ptr(layout, ot, NULL, 0, "type", PAR_LATTICE);
|
||||
}
|
||||
else if (parent->type == OB_MESH) {
|
||||
if (has_children_of_type.curves) {
|
||||
uiItemO(layout, "Object (Attach Curves to Surface)", ICON_NONE, "CURVES_OT_surface_set");
|
||||
}
|
||||
}
|
||||
|
||||
/* vertex parenting */
|
||||
if (OB_TYPE_SUPPORT_PARVERT(parent->type)) {
|
||||
|
@@ -434,7 +434,10 @@ typedef struct Object {
|
||||
char empty_image_visibility_flag;
|
||||
char empty_image_depth;
|
||||
char empty_image_flag;
|
||||
char _pad8[5];
|
||||
|
||||
/** ObjectModifierFlag */
|
||||
uint8_t modifier_flag;
|
||||
char _pad8[4];
|
||||
|
||||
struct PreviewImage *preview;
|
||||
|
||||
@@ -788,6 +791,10 @@ enum {
|
||||
OB_EMPTY_IMAGE_USE_ALPHA_BLEND = 1 << 0,
|
||||
};
|
||||
|
||||
typedef enum ObjectModifierFlag {
|
||||
OB_MODIFIER_FLAG_ADD_REST_POSITION = 1 << 0,
|
||||
} ObjectModifierFlag;
|
||||
|
||||
#define MAX_DUPLI_RECUR 8
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
@@ -3579,6 +3579,14 @@ static void rna_def_object(BlenderRNA *brna)
|
||||
RNA_def_property_ui_text(prop, "Empty Image Side", "Show front/back side");
|
||||
RNA_def_property_update(prop, NC_OBJECT | ND_DRAW, NULL);
|
||||
|
||||
prop = RNA_def_property(srna, "add_rest_position_attribute", PROP_BOOLEAN, PROP_NONE);
|
||||
RNA_def_property_boolean_sdna(prop, NULL, "modifier_flag", OB_MODIFIER_FLAG_ADD_REST_POSITION);
|
||||
RNA_def_property_ui_text(prop,
|
||||
"Add Rest Position",
|
||||
"Add a \"rest_position\" attribute that is a copy of the position "
|
||||
"attribute before shape keys and modifiers are evaluated");
|
||||
RNA_def_property_update(prop, NC_OBJECT | ND_DRAW, "rna_Object_internal_update_data");
|
||||
|
||||
/* render */
|
||||
prop = RNA_def_property(srna, "pass_index", PROP_INT, PROP_UNSIGNED);
|
||||
RNA_def_property_int_sdna(prop, NULL, "index");
|
||||
|
@@ -21,6 +21,7 @@
|
||||
#include "BLI_utildefines.h"
|
||||
|
||||
#include "DNA_collection_types.h"
|
||||
#include "DNA_curves_types.h"
|
||||
#include "DNA_defaults.h"
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_mesh_types.h"
|
||||
@@ -190,6 +191,10 @@ static bool node_needs_own_transform_relation(const bNode &node)
|
||||
return storage.transform_space == GEO_NODE_TRANSFORM_SPACE_RELATIVE;
|
||||
}
|
||||
|
||||
if (node.type == GEO_NODE_DEFORM_CURVES_ON_SURFACE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -269,6 +274,14 @@ static void updateDepsgraph(ModifierData *md, const ModifierUpdateDepsgraphConte
|
||||
Set<ID *> used_ids;
|
||||
find_used_ids_from_settings(nmd->settings, used_ids);
|
||||
process_nodes_for_depsgraph(*nmd->node_group, used_ids, needs_own_transform_relation);
|
||||
|
||||
if (ctx->object->type == OB_CURVES) {
|
||||
Curves *curves_id = static_cast<Curves *>(ctx->object->data);
|
||||
if (curves_id->surface != nullptr) {
|
||||
used_ids.add(&curves_id->surface->id);
|
||||
}
|
||||
}
|
||||
|
||||
for (ID *id : used_ids) {
|
||||
switch ((ID_Type)GS(id->name)) {
|
||||
case ID_OB: {
|
||||
|
@@ -47,6 +47,7 @@ void register_node_type_geo_curve_subdivide(void);
|
||||
void register_node_type_geo_curve_to_mesh(void);
|
||||
void register_node_type_geo_curve_to_points(void);
|
||||
void register_node_type_geo_curve_trim(void);
|
||||
void register_node_type_geo_deform_curves_on_surface(void);
|
||||
void register_node_type_geo_delete_geometry(void);
|
||||
void register_node_type_geo_duplicate_elements(void);
|
||||
void register_node_type_geo_distribute_points_on_faces(void);
|
||||
|
@@ -301,6 +301,7 @@ DefNode(GeometryNode, GEO_NODE_CURVE_SPLINE_PARAMETER, 0, "SPLINE_PARAMETER", Sp
|
||||
DefNode(GeometryNode, GEO_NODE_CURVE_SPLINE_TYPE, def_geo_curve_spline_type, "CURVE_SPLINE_TYPE", CurveSplineType, "Set Spline Type", "")
|
||||
DefNode(GeometryNode, GEO_NODE_CURVE_TO_MESH, 0, "CURVE_TO_MESH", CurveToMesh, "Curve to Mesh", "")
|
||||
DefNode(GeometryNode, GEO_NODE_CURVE_TO_POINTS, def_geo_curve_to_points, "CURVE_TO_POINTS", CurveToPoints, "Curve to Points", "")
|
||||
DefNode(GeometryNode, GEO_NODE_DEFORM_CURVES_ON_SURFACE, 0, "DEFORM_CURVES_ON_SURFACE", DeformCurvesOnSurface, "Deform Curves on Surface", "")
|
||||
DefNode(GeometryNode, GEO_NODE_DELETE_GEOMETRY, def_geo_delete_geometry, "DELETE_GEOMETRY", DeleteGeometry, "Delete Geometry", "")
|
||||
DefNode(GeometryNode, GEO_NODE_DUPLICATE_ELEMENTS, def_geo_duplicate_elements, "DUPLICATE_ELEMENTS", DuplicateElements, "Duplicate Elements", "")
|
||||
DefNode(GeometryNode, GEO_NODE_DISTRIBUTE_POINTS_ON_FACES, def_geo_distribute_points_on_faces, "DISTRIBUTE_POINTS_ON_FACES", DistributePointsOnFaces, "Distribute Points on Faces", "")
|
||||
|
@@ -57,6 +57,7 @@ set(SRC
|
||||
nodes/node_geo_curve_to_mesh.cc
|
||||
nodes/node_geo_curve_to_points.cc
|
||||
nodes/node_geo_curve_trim.cc
|
||||
nodes/node_geo_deform_curves_with_surface.cc
|
||||
nodes/node_geo_delete_geometry.cc
|
||||
nodes/node_geo_distribute_points_on_faces.cc
|
||||
nodes/node_geo_dual_mesh.cc
|
||||
|
@@ -0,0 +1,345 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BKE_attribute_math.hh"
|
||||
#include "BKE_curves.hh"
|
||||
#include "BKE_editmesh.h"
|
||||
#include "BKE_lib_id.h"
|
||||
#include "BKE_mesh.h"
|
||||
#include "BKE_mesh_runtime.h"
|
||||
#include "BKE_mesh_wrapper.h"
|
||||
#include "BKE_modifier.h"
|
||||
#include "BKE_type_conversions.hh"
|
||||
|
||||
#include "BLI_float3x3.hh"
|
||||
#include "BLI_task.hh"
|
||||
|
||||
#include "UI_interface.h"
|
||||
#include "UI_resources.h"
|
||||
|
||||
#include "DNA_mesh_types.h"
|
||||
#include "DNA_meshdata_types.h"
|
||||
|
||||
#include "NOD_socket_search_link.hh"
|
||||
|
||||
#include "GEO_reverse_uv_sampler.hh"
|
||||
|
||||
#include "DEG_depsgraph_query.h"
|
||||
|
||||
#include "node_geometry_util.hh"
|
||||
|
||||
namespace blender::nodes::node_geo_deform_curves_with_surface_cc {
|
||||
|
||||
using attribute_math::mix3;
|
||||
using bke::CurvesGeometry;
|
||||
using geometry::ReverseUVSampler;
|
||||
|
||||
NODE_STORAGE_FUNCS(NodeGeometryCurveTrim)
|
||||
|
||||
static void node_declare(NodeDeclarationBuilder &b)
|
||||
{
|
||||
b.add_input<decl::Geometry>(N_("Curves")).supported_type(GEO_COMPONENT_TYPE_CURVE);
|
||||
b.add_output<decl::Geometry>(N_("Curves"));
|
||||
}
|
||||
|
||||
static void deform_curves(CurvesGeometry &curves,
|
||||
const Mesh &surface_mesh_old,
|
||||
const Mesh &surface_mesh_new,
|
||||
const Span<float2> curve_attachment_uvs,
|
||||
const ReverseUVSampler &reverse_uv_sampler_old,
|
||||
const ReverseUVSampler &reverse_uv_sampler_new,
|
||||
const Span<float3> corner_normals_old,
|
||||
const Span<float3> corner_normals_new,
|
||||
const Span<float3> rest_positions,
|
||||
const float4x4 &surface_to_curves,
|
||||
std::atomic<int> &r_invalid_uv_count)
|
||||
{
|
||||
/* Find attachment points on old and new mesh. */
|
||||
const int curves_num = curves.curves_num();
|
||||
Array<ReverseUVSampler::Result> surface_samples_old(curves_num);
|
||||
Array<ReverseUVSampler::Result> surface_samples_new(curves_num);
|
||||
threading::parallel_invoke(
|
||||
[&]() { reverse_uv_sampler_old.sample_many(curve_attachment_uvs, surface_samples_old); },
|
||||
[&]() { reverse_uv_sampler_new.sample_many(curve_attachment_uvs, surface_samples_new); });
|
||||
|
||||
MutableSpan<float3> positions = curves.positions_for_write();
|
||||
|
||||
const float4x4 curves_to_surface = surface_to_curves.inverted();
|
||||
|
||||
threading::parallel_for(curves.curves_range(), 256, [&](const IndexRange range) {
|
||||
for (const int curve_i : range) {
|
||||
const ReverseUVSampler::Result &surface_sample_old = surface_samples_old[curve_i];
|
||||
if (surface_sample_old.type != ReverseUVSampler::ResultType::Ok) {
|
||||
r_invalid_uv_count++;
|
||||
continue;
|
||||
}
|
||||
const ReverseUVSampler::Result &surface_sample_new = surface_samples_new[curve_i];
|
||||
if (surface_sample_new.type != ReverseUVSampler::ResultType::Ok) {
|
||||
r_invalid_uv_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const MLoopTri &looptri_old = *surface_sample_old.looptri;
|
||||
const MLoopTri &looptri_new = *surface_sample_new.looptri;
|
||||
const float3 &bary_weights_old = surface_sample_old.bary_weights;
|
||||
const float3 &bary_weights_new = surface_sample_new.bary_weights;
|
||||
|
||||
const int corner_0_old = looptri_old.tri[0];
|
||||
const int corner_1_old = looptri_old.tri[1];
|
||||
const int corner_2_old = looptri_old.tri[2];
|
||||
|
||||
const int corner_0_new = looptri_new.tri[0];
|
||||
const int corner_1_new = looptri_new.tri[1];
|
||||
const int corner_2_new = looptri_new.tri[2];
|
||||
|
||||
const int vert_0_old = surface_mesh_old.mloop[corner_0_old].v;
|
||||
const int vert_1_old = surface_mesh_old.mloop[corner_1_old].v;
|
||||
const int vert_2_old = surface_mesh_old.mloop[corner_2_old].v;
|
||||
|
||||
const int vert_0_new = surface_mesh_new.mloop[corner_0_new].v;
|
||||
const int vert_1_new = surface_mesh_new.mloop[corner_1_new].v;
|
||||
const int vert_2_new = surface_mesh_new.mloop[corner_2_new].v;
|
||||
|
||||
const float3 &normal_0_old = corner_normals_old[corner_0_old];
|
||||
const float3 &normal_1_old = corner_normals_old[corner_1_old];
|
||||
const float3 &normal_2_old = corner_normals_old[corner_2_old];
|
||||
const float3 normal_old = math::normalize(
|
||||
mix3(bary_weights_old, normal_0_old, normal_1_old, normal_2_old));
|
||||
|
||||
const float3 &normal_0_new = corner_normals_new[corner_0_new];
|
||||
const float3 &normal_1_new = corner_normals_new[corner_1_new];
|
||||
const float3 &normal_2_new = corner_normals_new[corner_2_new];
|
||||
const float3 normal_new = math::normalize(
|
||||
mix3(bary_weights_new, normal_0_new, normal_1_new, normal_2_new));
|
||||
|
||||
const float3 &pos_0_old = surface_mesh_old.mvert[vert_0_old].co;
|
||||
const float3 &pos_1_old = surface_mesh_old.mvert[vert_1_old].co;
|
||||
const float3 &pos_2_old = surface_mesh_old.mvert[vert_2_old].co;
|
||||
const float3 pos_old = mix3(bary_weights_old, pos_0_old, pos_1_old, pos_2_old);
|
||||
|
||||
const float3 &pos_0_new = surface_mesh_new.mvert[vert_0_new].co;
|
||||
const float3 &pos_1_new = surface_mesh_new.mvert[vert_1_new].co;
|
||||
const float3 &pos_2_new = surface_mesh_new.mvert[vert_2_new].co;
|
||||
const float3 pos_new = mix3(bary_weights_new, pos_0_new, pos_1_new, pos_2_new);
|
||||
|
||||
/* The translation is just the difference between the old and new position on the surface. */
|
||||
const float3 translation = pos_new - pos_old;
|
||||
|
||||
const float3 &rest_pos_0 = rest_positions[vert_0_new];
|
||||
const float3 &rest_pos_1 = rest_positions[vert_1_new];
|
||||
|
||||
/* The tangent reference direction is used to determine the rotation of the surface point
|
||||
* around its normal axis. It's important that the old and new tangent reference are computed
|
||||
* in a consistent way. If the surface has not been rotated, the old and new tangent
|
||||
* reference have to have the same direction. For that reason, the old tangent reference is
|
||||
* computed based on the rest position attribute instead of positions on the old mesh. This
|
||||
* way the old and new tangent reference use the same topology.
|
||||
*
|
||||
* TODO: Figure out if this can be smoothly interpolated across the surface as well.
|
||||
* Currently, this is a source of discontinuity in the deformation, because the vector
|
||||
* changes intantly from one triangle to the next. */
|
||||
const float3 tangent_reference_dir_old = rest_pos_1 - rest_pos_0;
|
||||
const float3 tangent_reference_dir_new = pos_1_new - pos_0_new;
|
||||
|
||||
/* Compute first local tangent based on the (potentially smoothed) normal and the tangent
|
||||
* reference. */
|
||||
const float3 tangent_x_old = math::normalize(
|
||||
math::cross(normal_old, tangent_reference_dir_old));
|
||||
const float3 tangent_x_new = math::normalize(
|
||||
math::cross(normal_new, tangent_reference_dir_new));
|
||||
|
||||
/* The second tangent defined by the normal and first tangent. */
|
||||
const float3 tangent_y_old = math::normalize(math::cross(normal_old, tangent_x_old));
|
||||
const float3 tangent_y_new = math::normalize(math::cross(normal_new, tangent_x_new));
|
||||
|
||||
/* Construct rotation matrix that encodes the orientation of the old surface position. */
|
||||
float3x3 rotation_old;
|
||||
copy_v3_v3(rotation_old.values[0], tangent_x_old);
|
||||
copy_v3_v3(rotation_old.values[1], tangent_y_old);
|
||||
copy_v3_v3(rotation_old.values[2], normal_old);
|
||||
|
||||
/* Construct rotation matrix that encodes the orientation of the new surface position. */
|
||||
float3x3 rotation_new;
|
||||
copy_v3_v3(rotation_new.values[0], tangent_x_new);
|
||||
copy_v3_v3(rotation_new.values[1], tangent_y_new);
|
||||
copy_v3_v3(rotation_new.values[2], normal_new);
|
||||
|
||||
/* Can use transpose instead of inverse because the matrix is orthonormal. */
|
||||
const float3x3 rotation_old_inv = rotation_old.transposed();
|
||||
|
||||
/* Compute a rotation matrix that rotates points from the old to the new surface
|
||||
* orientation. */
|
||||
const float3x3 rotation = rotation_new * rotation_old_inv;
|
||||
float4x4 rotation_4x4;
|
||||
copy_m4_m3(rotation_4x4.values, rotation.values);
|
||||
|
||||
/* Construction transformation matrix for this surface position that includes rotation and
|
||||
* translation. */
|
||||
float4x4 surface_transform = float4x4::identity();
|
||||
sub_v3_v3(surface_transform.values[3], pos_old);
|
||||
mul_m4_m4_pre(surface_transform.values, rotation_4x4.values);
|
||||
add_v3_v3(surface_transform.values[3], translation);
|
||||
add_v3_v3(surface_transform.values[3], pos_old);
|
||||
|
||||
/* Change the basis of the transformation so to that it can be applied in the local space of
|
||||
* the curves. */
|
||||
const float4x4 curve_transform = surface_to_curves * surface_transform * curves_to_surface;
|
||||
|
||||
/* Actually transform all points. */
|
||||
const IndexRange points = curves.points_for_curve(curve_i);
|
||||
for (const int point_i : points) {
|
||||
const float3 old_point_pos = positions[point_i];
|
||||
const float3 new_point_pos = curve_transform * old_point_pos;
|
||||
positions[point_i] = new_point_pos;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void node_geo_exec(GeoNodeExecParams params)
|
||||
{
|
||||
GeometrySet curves_geometry = params.extract_input<GeometrySet>("Curves");
|
||||
|
||||
Mesh *surface_mesh_orig = nullptr;
|
||||
bool free_suface_mesh_orig = false;
|
||||
|
||||
auto pass_through_input = [&]() {
|
||||
params.set_output("Curves", std::move(curves_geometry));
|
||||
if (free_suface_mesh_orig) {
|
||||
BKE_id_free(nullptr, surface_mesh_orig);
|
||||
}
|
||||
};
|
||||
|
||||
const Object *self_ob_eval = params.self_object();
|
||||
if (self_ob_eval == nullptr || self_ob_eval->type != OB_CURVES) {
|
||||
pass_through_input();
|
||||
return;
|
||||
}
|
||||
const Curves *self_curves_eval = static_cast<const Curves *>(self_ob_eval->data);
|
||||
Object *surface_ob_eval = self_curves_eval->surface;
|
||||
const StringRef uv_map_name = self_curves_eval->surface_uv_map;
|
||||
const StringRef rest_position_name = "rest_position";
|
||||
|
||||
if (!curves_geometry.has_curves()) {
|
||||
pass_through_input();
|
||||
return;
|
||||
}
|
||||
if (surface_ob_eval == nullptr || surface_ob_eval->type != OB_MESH) {
|
||||
pass_through_input();
|
||||
return;
|
||||
}
|
||||
Object *surface_ob_orig = DEG_get_original_object(surface_ob_eval);
|
||||
Mesh &surface_object_data = *static_cast<Mesh *>(surface_ob_orig->data);
|
||||
|
||||
if (BMEditMesh *em = surface_object_data.edit_mesh) {
|
||||
surface_mesh_orig = BKE_mesh_from_bmesh_for_eval_nomain(em->bm, NULL, &surface_object_data);
|
||||
free_suface_mesh_orig = true;
|
||||
}
|
||||
else {
|
||||
surface_mesh_orig = &surface_object_data;
|
||||
}
|
||||
Mesh *surface_mesh_eval = BKE_modifier_get_evaluated_mesh_from_evaluated_object(surface_ob_eval,
|
||||
false);
|
||||
if (surface_mesh_eval == nullptr) {
|
||||
pass_through_input();
|
||||
return;
|
||||
}
|
||||
|
||||
BKE_mesh_wrapper_ensure_mdata(surface_mesh_eval);
|
||||
|
||||
MeshComponent mesh_eval;
|
||||
mesh_eval.replace(surface_mesh_eval, GeometryOwnershipType::ReadOnly);
|
||||
MeshComponent mesh_orig;
|
||||
mesh_orig.replace(surface_mesh_orig, GeometryOwnershipType::ReadOnly);
|
||||
|
||||
Curves &curves_id = *curves_geometry.get_curves_for_write();
|
||||
CurvesGeometry &curves = CurvesGeometry::wrap(curves_id.geometry);
|
||||
|
||||
if (!mesh_orig.attribute_exists(uv_map_name)) {
|
||||
pass_through_input();
|
||||
return;
|
||||
}
|
||||
if (!mesh_eval.attribute_exists(uv_map_name)) {
|
||||
pass_through_input();
|
||||
return;
|
||||
}
|
||||
if (!mesh_eval.attribute_exists(rest_position_name)) {
|
||||
pass_through_input();
|
||||
return;
|
||||
}
|
||||
if (curves.surface_uv_coords().is_empty()) {
|
||||
pass_through_input();
|
||||
return;
|
||||
}
|
||||
const VArraySpan<float2> uv_map_orig = mesh_orig.attribute_get_for_read<float2>(
|
||||
uv_map_name, ATTR_DOMAIN_CORNER, {0.0f, 0.0f});
|
||||
const VArraySpan<float2> uv_map_eval = mesh_eval.attribute_get_for_read<float2>(
|
||||
uv_map_name, ATTR_DOMAIN_CORNER, {0.0f, 0.0f});
|
||||
const VArraySpan<float3> rest_positions = mesh_eval.attribute_get_for_read<float3>(
|
||||
rest_position_name, ATTR_DOMAIN_POINT, {0.0f, 0.0f, 0.0f});
|
||||
const Span<float2> surface_uv_coords = curves.surface_uv_coords();
|
||||
|
||||
const Span<MLoopTri> looptris_orig{BKE_mesh_runtime_looptri_ensure(surface_mesh_orig),
|
||||
BKE_mesh_runtime_looptri_len(surface_mesh_orig)};
|
||||
const Span<MLoopTri> looptris_eval{BKE_mesh_runtime_looptri_ensure(surface_mesh_eval),
|
||||
BKE_mesh_runtime_looptri_len(surface_mesh_eval)};
|
||||
const ReverseUVSampler reverse_uv_sampler_orig{uv_map_orig, looptris_orig};
|
||||
const ReverseUVSampler reverse_uv_sampler_eval{uv_map_eval, looptris_eval};
|
||||
|
||||
/* Retrieve face corner normals from each mesh. It's necessary to use face corner normals
|
||||
* because face normals or vertex normals may lose information (custom normals, auto smooth) in
|
||||
* some cases. It isn't yet possible to retrieve lazily calculated face corner normals from a
|
||||
* const mesh, so they are calculated here every time. */
|
||||
Array<float3> corner_normals_orig(surface_mesh_orig->totloop);
|
||||
Array<float3> corner_normals_eval(surface_mesh_eval->totloop);
|
||||
BKE_mesh_calc_normals_split_ex(
|
||||
surface_mesh_orig, nullptr, reinterpret_cast<float(*)[3]>(corner_normals_orig.data()));
|
||||
BKE_mesh_calc_normals_split_ex(
|
||||
surface_mesh_eval, nullptr, reinterpret_cast<float(*)[3]>(corner_normals_eval.data()));
|
||||
|
||||
std::atomic<int> invalid_uv_count = 0;
|
||||
|
||||
const bke::CurvesSurfaceTransforms transforms{*self_ob_eval, surface_ob_eval};
|
||||
|
||||
deform_curves(curves,
|
||||
*surface_mesh_orig,
|
||||
*surface_mesh_eval,
|
||||
surface_uv_coords,
|
||||
reverse_uv_sampler_orig,
|
||||
reverse_uv_sampler_eval,
|
||||
corner_normals_orig,
|
||||
corner_normals_eval,
|
||||
rest_positions,
|
||||
transforms.surface_to_curves,
|
||||
invalid_uv_count);
|
||||
|
||||
curves.tag_positions_changed();
|
||||
|
||||
if (invalid_uv_count) {
|
||||
char *error = BLI_sprintfN(
|
||||
TIP_("Invalid surface attachment UV coordinates found on %d curves"),
|
||||
invalid_uv_count.load());
|
||||
params.error_message_add(NodeWarningType::Warning, error);
|
||||
MEM_freeN(error);
|
||||
}
|
||||
|
||||
if (free_suface_mesh_orig) {
|
||||
BKE_id_free(nullptr, surface_mesh_orig);
|
||||
}
|
||||
|
||||
params.set_output("Curves", curves_geometry);
|
||||
}
|
||||
|
||||
} // namespace blender::nodes::node_geo_deform_curves_with_surface_cc
|
||||
|
||||
void register_node_type_geo_deform_curves_on_surface()
|
||||
{
|
||||
namespace file_ns = blender::nodes::node_geo_deform_curves_with_surface_cc;
|
||||
|
||||
static bNodeType ntype;
|
||||
geo_node_type_base(
|
||||
&ntype, GEO_NODE_DEFORM_CURVES_ON_SURFACE, "Deform Curves on Surface", NODE_CLASS_GEOMETRY);
|
||||
ntype.geometry_node_execute = file_ns::node_geo_exec;
|
||||
ntype.declare = file_ns::node_declare;
|
||||
node_type_size(&ntype, 170, 120, 700);
|
||||
nodeRegisterType(&ntype);
|
||||
}
|
Reference in New Issue
Block a user