Compare commits
12 Commits
geometry-n
...
attribute-
Author | SHA1 | Date | |
---|---|---|---|
04147e2864 | |||
9a157d6532 | |||
7234fd5b31 | |||
6b8a52e2b1 | |||
2eb00bd479 | |||
d755281413 | |||
0e9567c63c | |||
1faab5c82d | |||
d3e05090a8 | |||
a889254974 | |||
3c020f9416 | |||
8786624534 |
@@ -499,6 +499,9 @@ geometry_node_categories = [
|
||||
NodeItem("GeometryNodePointDistribute"),
|
||||
NodeItem("GeometryNodePointInstance"),
|
||||
]),
|
||||
GeometryNodeCategory("GEO_ATTRIBUTES", "Attributes", items=[
|
||||
NodeItem("GeometryNodeRandomAttribute"),
|
||||
]),
|
||||
GeometryNodeCategory("GEO_MATH", "Misc", items=[
|
||||
NodeItem("ShaderNodeMapRange"),
|
||||
NodeItem("ShaderNodeClamp"),
|
||||
|
212
source/blender/blenkernel/BKE_attribute_accessor.hh
Normal file
212
source/blender/blenkernel/BKE_attribute_accessor.hh
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "FN_cpp_type.hh"
|
||||
|
||||
#include "BKE_attribute.h"
|
||||
#include "BKE_geometry_set.hh"
|
||||
|
||||
struct Mesh;
|
||||
|
||||
namespace blender::bke {
|
||||
|
||||
using fn::CPPType;
|
||||
|
||||
class ReadAttribute {
|
||||
protected:
|
||||
const AttributeDomain domain_;
|
||||
const CPPType &cpp_type_;
|
||||
const int64_t size_;
|
||||
|
||||
public:
|
||||
ReadAttribute(AttributeDomain domain, const CPPType &cpp_type, const int64_t size)
|
||||
: domain_(domain), cpp_type_(cpp_type), size_(size)
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~ReadAttribute();
|
||||
|
||||
AttributeDomain domain() const
|
||||
{
|
||||
return domain_;
|
||||
}
|
||||
|
||||
const CPPType &cpp_type() const
|
||||
{
|
||||
return cpp_type_;
|
||||
}
|
||||
|
||||
int64_t size() const
|
||||
{
|
||||
return size_;
|
||||
}
|
||||
|
||||
void get(const int64_t index, void *r_value) const
|
||||
{
|
||||
BLI_assert(index < size_);
|
||||
this->get_internal(index, r_value);
|
||||
}
|
||||
|
||||
protected:
|
||||
/* r_value is expected to be uninitialized. */
|
||||
virtual void get_internal(const int64_t index, void *r_value) const = 0;
|
||||
};
|
||||
|
||||
class WriteAttribute {
|
||||
protected:
|
||||
const AttributeDomain domain_;
|
||||
const CPPType &cpp_type_;
|
||||
const int64_t size_;
|
||||
|
||||
public:
|
||||
WriteAttribute(AttributeDomain domain, const CPPType &cpp_type, const int64_t size)
|
||||
: domain_(domain), cpp_type_(cpp_type), size_(size)
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~WriteAttribute();
|
||||
|
||||
AttributeDomain domain() const
|
||||
{
|
||||
return domain_;
|
||||
}
|
||||
|
||||
const CPPType &cpp_type() const
|
||||
{
|
||||
return cpp_type_;
|
||||
}
|
||||
|
||||
int64_t size() const
|
||||
{
|
||||
return size_;
|
||||
}
|
||||
|
||||
void get(const int64_t index, void *r_value) const
|
||||
{
|
||||
BLI_assert(index < size_);
|
||||
this->get_internal(index, r_value);
|
||||
}
|
||||
|
||||
void set(const int64_t index, const void *value)
|
||||
{
|
||||
BLI_assert(index < size_);
|
||||
this->set_internal(index, value);
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void get_internal(const int64_t index, void *r_value) const = 0;
|
||||
virtual void set_internal(const int64_t index, const void *value) = 0;
|
||||
};
|
||||
|
||||
using ReadAttributePtr = std::unique_ptr<ReadAttribute>;
|
||||
using WriteAttributePtr = std::unique_ptr<WriteAttribute>;
|
||||
|
||||
template<typename T> class TypedReadAttribute {
|
||||
private:
|
||||
ReadAttributePtr attribute_;
|
||||
|
||||
public:
|
||||
TypedReadAttribute(ReadAttributePtr attribute) : attribute_(std::move(attribute))
|
||||
{
|
||||
BLI_assert(attribute_);
|
||||
BLI_assert(attribute_->cpp_type().is<T>());
|
||||
}
|
||||
|
||||
int64_t size() const
|
||||
{
|
||||
return attribute_->size();
|
||||
}
|
||||
|
||||
T operator[](const int64_t index) const
|
||||
{
|
||||
BLI_assert(index < attribute_->size());
|
||||
T value;
|
||||
value.~T();
|
||||
attribute_->get(index, &value);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T> class TypedWriteAttribute {
|
||||
private:
|
||||
WriteAttributePtr attribute_;
|
||||
|
||||
public:
|
||||
TypedWriteAttribute(WriteAttributePtr attribute) : attribute_(std::move(attribute))
|
||||
{
|
||||
BLI_assert(attribute_);
|
||||
BLI_assert(attribute_->cpp_type().is<T>());
|
||||
}
|
||||
|
||||
int64_t size() const
|
||||
{
|
||||
return attribute_->size();
|
||||
}
|
||||
|
||||
T operator[](const int64_t index) const
|
||||
{
|
||||
BLI_assert(index < attribute_->size());
|
||||
T value;
|
||||
value.~T();
|
||||
attribute_->get(index, &value);
|
||||
return value;
|
||||
}
|
||||
|
||||
void set(const int64_t index, const T &value)
|
||||
{
|
||||
attribute_->set(index, &value);
|
||||
}
|
||||
};
|
||||
|
||||
using FloatReadAttribute = TypedReadAttribute<float>;
|
||||
using Float3ReadAttribute = TypedReadAttribute<float3>;
|
||||
using FloatWriteAttribute = TypedWriteAttribute<float>;
|
||||
using Float3WriteAttribute = TypedWriteAttribute<float3>;
|
||||
|
||||
ReadAttributePtr mesh_attribute_get_for_read(const MeshComponent &mesh_component,
|
||||
const StringRef attribute_name);
|
||||
std::optional<WriteAttributePtr> mesh_attribute_get_for_write(MeshComponent &mesh_component,
|
||||
const StringRef attribute_name);
|
||||
|
||||
ReadAttributePtr mesh_attribute_adapt_domain(const MeshComponent &mesh_component,
|
||||
ReadAttributePtr attribute,
|
||||
const AttributeDomain to_domain);
|
||||
|
||||
ReadAttributePtr mesh_attribute_get_for_read(const MeshComponent &mesh_component,
|
||||
const StringRef attribute_name,
|
||||
const CPPType &cpp_type,
|
||||
const AttributeDomain domain,
|
||||
const void *default_value = nullptr);
|
||||
|
||||
template<typename T>
|
||||
TypedReadAttribute<T> mesh_attribute_get_for_read(const MeshComponent &mesh_component,
|
||||
const StringRef attribute_name,
|
||||
const AttributeDomain domain,
|
||||
const T &default_value)
|
||||
{
|
||||
ReadAttributePtr attribute = mesh_attribute_get_for_read(
|
||||
mesh_component,
|
||||
attribute_name,
|
||||
CPPType::get<T>(),
|
||||
domain,
|
||||
static_cast<const void *>(&default_value));
|
||||
BLI_assert(attribute);
|
||||
return attribute;
|
||||
}
|
||||
|
||||
} // namespace blender::bke
|
@@ -1345,6 +1345,7 @@ int ntreeTexExecTree(struct bNodeTree *ntree,
|
||||
#define GEO_NODE_POINT_INSTANCE 1005
|
||||
#define GEO_NODE_SUBDIVISION_SURFACE 1006
|
||||
#define GEO_NODE_OBJECT_INFO 1007
|
||||
#define GEO_NODE_RANDOM_ATTRIBUTE 1008
|
||||
|
||||
/** \} */
|
||||
|
||||
|
@@ -78,6 +78,7 @@ set(SRC
|
||||
intern/armature_deform.c
|
||||
intern/armature_update.c
|
||||
intern/attribute.c
|
||||
intern/attribute_accessor.cc
|
||||
intern/autoexec.c
|
||||
intern/blender.c
|
||||
intern/blender_copybuffer.c
|
||||
@@ -267,6 +268,7 @@ set(SRC
|
||||
BKE_appdir.h
|
||||
BKE_armature.h
|
||||
BKE_attribute.h
|
||||
BKE_attribute_accessor.hh
|
||||
BKE_autoexec.h
|
||||
BKE_blender.h
|
||||
BKE_blender_copybuffer.h
|
||||
|
478
source/blender/blenkernel/intern/attribute_accessor.cc
Normal file
478
source/blender/blenkernel/intern/attribute_accessor.cc
Normal file
@@ -0,0 +1,478 @@
|
||||
/*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "BKE_attribute_accessor.hh"
|
||||
#include "BKE_deform.h"
|
||||
|
||||
#include "DNA_mesh_types.h"
|
||||
#include "DNA_meshdata_types.h"
|
||||
|
||||
#include "BLI_color.hh"
|
||||
#include "BLI_float2.hh"
|
||||
#include "BLI_span.hh"
|
||||
|
||||
namespace blender::bke {
|
||||
|
||||
ReadAttribute::~ReadAttribute() = default;
|
||||
WriteAttribute::~WriteAttribute() = default;
|
||||
|
||||
class VertexWeightWriteAttribute final : public WriteAttribute {
|
||||
private:
|
||||
MutableSpan<MDeformVert> dverts_;
|
||||
const int dvert_index_;
|
||||
|
||||
public:
|
||||
VertexWeightWriteAttribute(MDeformVert *dverts, const int totvert, const int dvert_index)
|
||||
: WriteAttribute(ATTR_DOMAIN_VERTEX, CPPType::get<float>(), totvert),
|
||||
dverts_(dverts, totvert),
|
||||
dvert_index_(dvert_index)
|
||||
{
|
||||
}
|
||||
|
||||
void get_internal(const int64_t index, void *r_value) const override
|
||||
{
|
||||
this->get_internal(dverts_, dvert_index_, index, r_value);
|
||||
}
|
||||
|
||||
void set_internal(const int64_t index, const void *value) override
|
||||
{
|
||||
MDeformWeight *weight = BKE_defvert_ensure_index(&dverts_[index], dvert_index_);
|
||||
weight->weight = *reinterpret_cast<const float *>(value);
|
||||
}
|
||||
|
||||
static void get_internal(const Span<MDeformVert> dverts,
|
||||
const int dvert_index,
|
||||
const int64_t index,
|
||||
void *r_value)
|
||||
{
|
||||
const MDeformVert &dvert = dverts[index];
|
||||
for (const MDeformWeight &weight : Span(dvert.dw, dvert.totweight)) {
|
||||
if (weight.def_nr == dvert_index) {
|
||||
*(float *)r_value = weight.weight;
|
||||
return;
|
||||
}
|
||||
}
|
||||
*(float *)r_value = 0.0f;
|
||||
}
|
||||
};
|
||||
|
||||
class VertexWeightReadAttribute final : public ReadAttribute {
|
||||
private:
|
||||
const Span<MDeformVert> dverts_;
|
||||
const int dvert_index_;
|
||||
|
||||
public:
|
||||
VertexWeightReadAttribute(const MDeformVert *dverts, const int totvert, const int dvert_index)
|
||||
: ReadAttribute(ATTR_DOMAIN_VERTEX, CPPType::get<float>(), totvert),
|
||||
dverts_(dverts, totvert),
|
||||
dvert_index_(dvert_index)
|
||||
{
|
||||
}
|
||||
|
||||
void get_internal(const int64_t index, void *r_value) const override
|
||||
{
|
||||
VertexWeightWriteAttribute::get_internal(dverts_, dvert_index_, index, r_value);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T> class ArrayWriteAttribute final : public WriteAttribute {
|
||||
private:
|
||||
MutableSpan<T> data_;
|
||||
|
||||
public:
|
||||
ArrayWriteAttribute(AttributeDomain domain, MutableSpan<T> data)
|
||||
: WriteAttribute(domain, CPPType::get<T>(), data.size()), data_(data)
|
||||
{
|
||||
}
|
||||
|
||||
void get_internal(const int64_t index, void *r_value) const override
|
||||
{
|
||||
new (r_value) T(data_[index]);
|
||||
}
|
||||
|
||||
void set_internal(const int64_t index, const void *value) override
|
||||
{
|
||||
data_[index] = *reinterpret_cast<const T *>(value);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T> class ArrayReadAttribute final : public ReadAttribute {
|
||||
private:
|
||||
Span<T> data_;
|
||||
|
||||
public:
|
||||
ArrayReadAttribute(AttributeDomain domain, Span<T> data)
|
||||
: ReadAttribute(domain, CPPType::get<T>(), data.size()), data_(data)
|
||||
{
|
||||
}
|
||||
|
||||
void get_internal(const int64_t index, void *r_value) const override
|
||||
{
|
||||
new (r_value) T(data_[index]);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename StructT, typename ElemT, typename GetFuncT, typename SetFuncT>
|
||||
class DerivedArrayWriteAttribute final : public WriteAttribute {
|
||||
private:
|
||||
MutableSpan<StructT> data_;
|
||||
GetFuncT get_function_;
|
||||
SetFuncT set_function_;
|
||||
|
||||
public:
|
||||
DerivedArrayWriteAttribute(AttributeDomain domain,
|
||||
MutableSpan<StructT> data,
|
||||
GetFuncT get_function,
|
||||
SetFuncT set_function)
|
||||
: WriteAttribute(domain, CPPType::get<ElemT>(), data.size()),
|
||||
data_(data),
|
||||
get_function_(std::move(get_function)),
|
||||
set_function_(std::move(set_function))
|
||||
{
|
||||
}
|
||||
|
||||
void get_internal(const int64_t index, void *r_value) const override
|
||||
{
|
||||
const StructT &struct_value = data_[index];
|
||||
const ElemT value = get_function_(struct_value);
|
||||
new (r_value) ElemT(value);
|
||||
}
|
||||
|
||||
void set_internal(const int64_t index, const void *value) override
|
||||
{
|
||||
StructT &struct_value = data_[index];
|
||||
const ElemT &typed_value = *reinterpret_cast<const ElemT *>(value);
|
||||
set_function_(struct_value, typed_value);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename StructT, typename ElemT, typename GetFuncT>
|
||||
class DerivedArrayReadAttribute final : public ReadAttribute {
|
||||
private:
|
||||
Span<StructT> data_;
|
||||
GetFuncT get_function_;
|
||||
|
||||
public:
|
||||
DerivedArrayReadAttribute(AttributeDomain domain, Span<StructT> data, GetFuncT get_function)
|
||||
: ReadAttribute(domain, CPPType::get<ElemT>(), data.size()),
|
||||
data_(data),
|
||||
get_function_(std::move(get_function))
|
||||
{
|
||||
}
|
||||
|
||||
void get_internal(const int64_t index, void *r_value) const override
|
||||
{
|
||||
const StructT &struct_value = data_[index];
|
||||
const ElemT value = get_function_(struct_value);
|
||||
new (r_value) ElemT(value);
|
||||
}
|
||||
};
|
||||
|
||||
class ConstantReadAttribute final : public ReadAttribute {
|
||||
private:
|
||||
void *value_;
|
||||
|
||||
public:
|
||||
ConstantReadAttribute(AttributeDomain domain,
|
||||
const int64_t size,
|
||||
const CPPType &type,
|
||||
const void *value)
|
||||
: ReadAttribute(domain, type, size)
|
||||
{
|
||||
value_ = MEM_mallocN_aligned(type.size(), type.alignment(), __func__);
|
||||
type.copy_to_uninitialized(value, value_);
|
||||
}
|
||||
|
||||
void get_internal(const int64_t UNUSED(index), void *r_value) const override
|
||||
{
|
||||
this->cpp_type_.copy_to_uninitialized(value_, r_value);
|
||||
}
|
||||
};
|
||||
|
||||
static ReadAttributePtr mesh_attribute_custom_data_read(const CustomData &custom_data,
|
||||
const int size,
|
||||
const StringRef attribute_name,
|
||||
const AttributeDomain domain)
|
||||
{
|
||||
for (const CustomDataLayer &layer : Span(custom_data.layers, custom_data.totlayer)) {
|
||||
if (layer.name != nullptr && layer.name == attribute_name) {
|
||||
switch (layer.type) {
|
||||
case CD_PROP_FLOAT:
|
||||
return std::make_unique<ArrayReadAttribute<float>>(
|
||||
domain, Span(static_cast<float *>(layer.data), size));
|
||||
case CD_PROP_FLOAT2:
|
||||
return std::make_unique<ArrayReadAttribute<float2>>(
|
||||
domain, Span(static_cast<float2 *>(layer.data), size));
|
||||
case CD_PROP_FLOAT3:
|
||||
return std::make_unique<ArrayReadAttribute<float3>>(
|
||||
domain, Span(static_cast<float3 *>(layer.data), size));
|
||||
case CD_PROP_INT32:
|
||||
return std::make_unique<ArrayReadAttribute<int>>(
|
||||
domain, Span(static_cast<int *>(layer.data), size));
|
||||
case CD_PROP_COLOR:
|
||||
return std::make_unique<ArrayReadAttribute<Color4f>>(
|
||||
domain, Span(static_cast<Color4f *>(layer.data), size));
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static WriteAttributePtr mesh_attribute_custom_data_write(CustomData custom_data,
|
||||
const int size,
|
||||
const StringRef attribute_name,
|
||||
const AttributeDomain domain)
|
||||
{
|
||||
for (const CustomDataLayer &layer : Span(custom_data.layers, custom_data.totlayer)) {
|
||||
if (layer.name != nullptr && layer.name == attribute_name) {
|
||||
switch (layer.type) {
|
||||
case CD_PROP_FLOAT:
|
||||
return std::make_unique<ArrayWriteAttribute<float>>(
|
||||
domain, MutableSpan(static_cast<float *>(layer.data), size));
|
||||
case CD_PROP_FLOAT2:
|
||||
return std::make_unique<ArrayWriteAttribute<float2>>(
|
||||
domain, MutableSpan(static_cast<float2 *>(layer.data), size));
|
||||
case CD_PROP_FLOAT3:
|
||||
return std::make_unique<ArrayWriteAttribute<float3>>(
|
||||
domain, MutableSpan(static_cast<float3 *>(layer.data), size));
|
||||
case CD_PROP_INT32:
|
||||
return std::make_unique<ArrayWriteAttribute<int>>(
|
||||
domain, MutableSpan(static_cast<int *>(layer.data), size));
|
||||
case CD_PROP_COLOR:
|
||||
return std::make_unique<ArrayWriteAttribute<Color4f>>(
|
||||
domain, MutableSpan(static_cast<Color4f *>(layer.data), size));
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
ReadAttributePtr mesh_attribute_get_for_read(const MeshComponent &mesh_component,
|
||||
const StringRef attribute_name)
|
||||
{
|
||||
const Mesh *mesh = mesh_component.get_for_read();
|
||||
if (mesh == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
ReadAttributePtr corner_attribute = mesh_attribute_custom_data_read(
|
||||
mesh->ldata, mesh->totloop, attribute_name, ATTR_DOMAIN_CORNER);
|
||||
if (corner_attribute) {
|
||||
return corner_attribute;
|
||||
}
|
||||
|
||||
if (attribute_name == "Position") {
|
||||
auto get_vertex_position = [](const MVert &vert) { return float3(vert.co); };
|
||||
return std::make_unique<
|
||||
DerivedArrayReadAttribute<MVert, float3, decltype(get_vertex_position)>>(
|
||||
ATTR_DOMAIN_VERTEX, Span(mesh->mvert, mesh->totvert), get_vertex_position);
|
||||
}
|
||||
|
||||
const int vertex_group_index = mesh_component.vertex_group_index(attribute_name);
|
||||
if (vertex_group_index >= 0) {
|
||||
return std::make_unique<VertexWeightReadAttribute>(
|
||||
mesh->dvert, mesh->totvert, vertex_group_index);
|
||||
}
|
||||
|
||||
ReadAttributePtr vertex_attribute = mesh_attribute_custom_data_read(
|
||||
mesh->vdata, mesh->totvert, attribute_name, ATTR_DOMAIN_VERTEX);
|
||||
if (vertex_attribute) {
|
||||
return vertex_attribute;
|
||||
}
|
||||
|
||||
ReadAttributePtr edge_attribute = mesh_attribute_custom_data_read(
|
||||
mesh->edata, mesh->totedge, attribute_name, ATTR_DOMAIN_EDGE);
|
||||
if (edge_attribute) {
|
||||
return edge_attribute;
|
||||
}
|
||||
|
||||
ReadAttributePtr polygon_attribute = mesh_attribute_custom_data_read(
|
||||
mesh->pdata, mesh->totpoly, attribute_name, ATTR_DOMAIN_POLYGON);
|
||||
if (polygon_attribute) {
|
||||
return polygon_attribute;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<WriteAttributePtr> mesh_attribute_get_for_write(MeshComponent &mesh_component,
|
||||
const StringRef attribute_name)
|
||||
{
|
||||
Mesh *mesh = mesh_component.get_for_write();
|
||||
if (mesh == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
WriteAttributePtr corner_attribute = mesh_attribute_custom_data_write(
|
||||
mesh->ldata, mesh->totloop, attribute_name, ATTR_DOMAIN_CORNER);
|
||||
if (corner_attribute) {
|
||||
return corner_attribute;
|
||||
}
|
||||
|
||||
if (attribute_name == "Position") {
|
||||
auto get_vertex_position = [](const MVert &vert) { return float3(vert.co); };
|
||||
auto set_vertex_position = [](MVert &vert, const float3 &co) { copy_v3_v3(vert.co, co); };
|
||||
return std::make_unique<DerivedArrayWriteAttribute<MVert,
|
||||
float3,
|
||||
decltype(get_vertex_position),
|
||||
decltype(set_vertex_position)>>(
|
||||
ATTR_DOMAIN_VERTEX,
|
||||
MutableSpan(mesh->mvert, mesh->totvert),
|
||||
get_vertex_position,
|
||||
set_vertex_position);
|
||||
}
|
||||
|
||||
const int vertex_group_index = mesh_component.vertex_group_index(attribute_name);
|
||||
if (vertex_group_index >= 0) {
|
||||
return std::make_unique<VertexWeightWriteAttribute>(
|
||||
mesh->dvert, mesh->totvert, vertex_group_index);
|
||||
}
|
||||
|
||||
WriteAttributePtr vertex_attribute = mesh_attribute_custom_data_write(
|
||||
mesh->vdata, mesh->totvert, attribute_name, ATTR_DOMAIN_VERTEX);
|
||||
if (vertex_attribute) {
|
||||
return vertex_attribute;
|
||||
}
|
||||
|
||||
WriteAttributePtr edge_attribute = mesh_attribute_custom_data_write(
|
||||
mesh->edata, mesh->totedge, attribute_name, ATTR_DOMAIN_EDGE);
|
||||
if (edge_attribute) {
|
||||
return edge_attribute;
|
||||
}
|
||||
|
||||
WriteAttributePtr polygon_attribute = mesh_attribute_custom_data_write(
|
||||
mesh->pdata, mesh->totpoly, attribute_name, ATTR_DOMAIN_POLYGON);
|
||||
if (polygon_attribute) {
|
||||
return polygon_attribute;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static ReadAttributePtr adapt_mesh_attribute_to_corner(const MeshComponent &UNUSED(mesh_component),
|
||||
ReadAttributePtr UNUSED(attribute))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
static ReadAttributePtr adapt_mesh_attribute_to_vertex(const MeshComponent &UNUSED(mesh_component),
|
||||
ReadAttributePtr UNUSED(attribute))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
static ReadAttributePtr adapt_mesh_attribute_to_edge(const MeshComponent &UNUSED(mesh_component),
|
||||
ReadAttributePtr UNUSED(attribute))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
static ReadAttributePtr adapt_mesh_attribute_to_polygon(
|
||||
const MeshComponent &UNUSED(mesh_component), ReadAttributePtr UNUSED(attribute))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
ReadAttributePtr mesh_attribute_adapt_domain(const MeshComponent &mesh_component,
|
||||
ReadAttributePtr attribute,
|
||||
const AttributeDomain to_domain)
|
||||
{
|
||||
if (!attribute) {
|
||||
return {};
|
||||
}
|
||||
const AttributeDomain from_domain = attribute->domain();
|
||||
if (from_domain == to_domain) {
|
||||
return attribute;
|
||||
}
|
||||
|
||||
switch (to_domain) {
|
||||
case ATTR_DOMAIN_CORNER:
|
||||
return adapt_mesh_attribute_to_corner(mesh_component, std::move(attribute));
|
||||
case ATTR_DOMAIN_VERTEX:
|
||||
return adapt_mesh_attribute_to_vertex(mesh_component, std::move(attribute));
|
||||
case ATTR_DOMAIN_EDGE:
|
||||
return adapt_mesh_attribute_to_edge(mesh_component, std::move(attribute));
|
||||
case ATTR_DOMAIN_POLYGON:
|
||||
return adapt_mesh_attribute_to_polygon(mesh_component, std::move(attribute));
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
static int get_domain_length(const MeshComponent &mesh_component, const AttributeDomain domain)
|
||||
{
|
||||
const Mesh *mesh = mesh_component.get_for_read();
|
||||
if (mesh == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
switch (domain) {
|
||||
case ATTR_DOMAIN_CORNER:
|
||||
return mesh->totloop;
|
||||
case ATTR_DOMAIN_VERTEX:
|
||||
return mesh->totvert;
|
||||
case ATTR_DOMAIN_EDGE:
|
||||
return mesh->totedge;
|
||||
case ATTR_DOMAIN_POLYGON:
|
||||
return mesh->totpoly;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ReadAttributePtr make_default_attribute(const MeshComponent &mesh_component,
|
||||
const AttributeDomain domain,
|
||||
const CPPType &cpp_type,
|
||||
const void *default_value)
|
||||
{
|
||||
|
||||
const int length = get_domain_length(mesh_component, domain);
|
||||
return std::make_unique<ConstantReadAttribute>(domain, length, cpp_type, default_value);
|
||||
}
|
||||
|
||||
ReadAttributePtr mesh_attribute_get_for_read(const MeshComponent &mesh_component,
|
||||
const StringRef attribute_name,
|
||||
const CPPType &cpp_type,
|
||||
const AttributeDomain domain,
|
||||
const void *default_value)
|
||||
{
|
||||
ReadAttributePtr attribute = mesh_attribute_get_for_read(mesh_component, attribute_name);
|
||||
auto get_default_or_empty = [&]() -> ReadAttributePtr {
|
||||
if (default_value != nullptr) {
|
||||
return make_default_attribute(mesh_component, domain, cpp_type, default_value);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
if (!attribute) {
|
||||
return get_default_or_empty();
|
||||
}
|
||||
if (attribute->domain() != domain) {
|
||||
attribute = mesh_attribute_adapt_domain(mesh_component, std::move(attribute), domain);
|
||||
}
|
||||
if (!attribute) {
|
||||
return get_default_or_empty();
|
||||
}
|
||||
if (attribute->cpp_type() != cpp_type) {
|
||||
/* TODO: Support some type conversions. */
|
||||
return get_default_or_empty();
|
||||
}
|
||||
return attribute;
|
||||
}
|
||||
|
||||
} // namespace blender::bke
|
@@ -4688,6 +4688,7 @@ static void registerGeometryNodes(void)
|
||||
register_node_type_geo_point_distribute();
|
||||
register_node_type_geo_point_instance();
|
||||
register_node_type_geo_object_info();
|
||||
register_node_type_geo_random_attribute();
|
||||
}
|
||||
|
||||
static void registerFunctionNodes(void)
|
||||
|
@@ -80,6 +80,13 @@ struct float2 {
|
||||
return *this;
|
||||
}
|
||||
|
||||
uint64_t hash() const
|
||||
{
|
||||
uint64_t x1 = *reinterpret_cast<const uint32_t *>(&x);
|
||||
uint64_t x2 = *reinterpret_cast<const uint32_t *>(&y);
|
||||
return (x1 * 812519) ^ (x2 * 707951);
|
||||
}
|
||||
|
||||
friend float2 operator+(const float2 &a, const float2 &b)
|
||||
{
|
||||
return {a.x + b.x, a.y + b.y};
|
||||
|
@@ -3163,6 +3163,14 @@ static void node_geometry_buts_triangulate(uiLayout *layout, bContext *UNUSED(C)
|
||||
uiItemR(layout, ptr, "ngon_method", DEFAULT_FLAGS, "", ICON_NONE);
|
||||
}
|
||||
|
||||
static void node_geometry_buts_random_attribute(uiLayout *layout,
|
||||
bContext *UNUSED(C),
|
||||
PointerRNA *ptr)
|
||||
{
|
||||
uiItemR(layout, ptr, "data_type", DEFAULT_FLAGS, "", ICON_NONE);
|
||||
uiItemR(layout, ptr, "domain", DEFAULT_FLAGS, "", ICON_NONE);
|
||||
}
|
||||
|
||||
static void node_geometry_set_butfunc(bNodeType *ntype)
|
||||
{
|
||||
switch (ntype->type) {
|
||||
@@ -3175,6 +3183,9 @@ static void node_geometry_set_butfunc(bNodeType *ntype)
|
||||
case GEO_NODE_TRIANGULATE:
|
||||
ntype->draw_buttons = node_geometry_buts_triangulate;
|
||||
break;
|
||||
case GEO_NODE_RANDOM_ATTRIBUTE:
|
||||
ntype->draw_buttons = node_geometry_buts_random_attribute;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -26,6 +26,7 @@ namespace blender::fn {
|
||||
MAKE_CPP_TYPE(bool, bool)
|
||||
|
||||
MAKE_CPP_TYPE(float, float)
|
||||
MAKE_CPP_TYPE(float2, blender::float2)
|
||||
MAKE_CPP_TYPE(float3, blender::float3)
|
||||
MAKE_CPP_TYPE(float4x4, blender::float4x4)
|
||||
|
||||
|
@@ -36,6 +36,7 @@
|
||||
#include "DNA_texture_types.h"
|
||||
|
||||
#include "BKE_animsys.h"
|
||||
#include "BKE_attribute.h"
|
||||
#include "BKE_image.h"
|
||||
#include "BKE_node.h"
|
||||
#include "BKE_texture.h"
|
||||
@@ -8258,6 +8259,30 @@ static void def_geo_triangulate(StructRNA *srna)
|
||||
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
|
||||
}
|
||||
|
||||
static void def_geo_attribute_create_common(StructRNA *srna)
|
||||
{
|
||||
PropertyRNA *prop;
|
||||
|
||||
prop = RNA_def_property(srna, "data_type", PROP_ENUM, PROP_NONE);
|
||||
RNA_def_property_enum_sdna(prop, NULL, "custom1");
|
||||
RNA_def_property_enum_items(prop, rna_enum_attribute_type_items);
|
||||
RNA_def_property_enum_default(prop, CD_PROP_FLOAT);
|
||||
RNA_def_property_ui_text(prop, "Data Type", "Type of data stored in attribute");
|
||||
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
|
||||
|
||||
prop = RNA_def_property(srna, "domain", PROP_ENUM, PROP_NONE);
|
||||
RNA_def_property_enum_sdna(prop, NULL, "custom2");
|
||||
RNA_def_property_enum_items(prop, rna_enum_attribute_domain_items);
|
||||
RNA_def_property_enum_default(prop, ATTR_DOMAIN_VERTEX);
|
||||
RNA_def_property_ui_text(prop, "Domain", "");
|
||||
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
|
||||
}
|
||||
|
||||
static void def_geo_random_attribute(StructRNA *srna)
|
||||
{
|
||||
def_geo_attribute_create_common(srna);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
static void rna_def_shader_node(BlenderRNA *brna)
|
||||
|
@@ -145,6 +145,7 @@ set(SRC
|
||||
geometry/nodes/node_geo_subdivision_surface.cc
|
||||
geometry/nodes/node_geo_point_distribute.cc
|
||||
geometry/nodes/node_geo_point_instance.cc
|
||||
geometry/nodes/node_geo_random_attribute.cc
|
||||
geometry/nodes/node_geo_transform.cc
|
||||
geometry/nodes/node_geo_triangulate.cc
|
||||
geometry/node_geometry_exec.cc
|
||||
|
@@ -34,6 +34,7 @@ void register_node_type_geo_triangulate(void);
|
||||
void register_node_type_geo_point_distribute(void);
|
||||
void register_node_type_geo_point_instance(void);
|
||||
void register_node_type_geo_object_info(void);
|
||||
void register_node_type_geo_random_attribute(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@
|
||||
|
||||
#include "FN_generic_value_map.hh"
|
||||
|
||||
#include "BKE_attribute_accessor.hh"
|
||||
#include "BKE_geometry_set.hh"
|
||||
#include "BKE_persistent_data_handle.hh"
|
||||
|
||||
@@ -25,8 +26,16 @@
|
||||
|
||||
namespace blender::nodes {
|
||||
|
||||
using bke::Float3ReadAttribute;
|
||||
using bke::Float3WriteAttribute;
|
||||
using bke::FloatReadAttribute;
|
||||
using bke::FloatWriteAttribute;
|
||||
using bke::PersistentDataHandleMap;
|
||||
using bke::PersistentObjectHandle;
|
||||
using bke::ReadAttribute;
|
||||
using bke::ReadAttributePtr;
|
||||
using bke::WriteAttribute;
|
||||
using bke::WriteAttributePtr;
|
||||
using fn::CPPType;
|
||||
using fn::GMutablePointer;
|
||||
using fn::GValueMap;
|
||||
|
@@ -274,6 +274,7 @@ DefNode(GeometryNode, GEO_NODE_BOOLEAN, def_geo_boolean, "BOOLEAN", Boolean, "Bo
|
||||
DefNode(GeometryNode, GEO_NODE_POINT_DISTRIBUTE, 0, "POINT_DISTRIBUTE", PointDistribute, "Point Distribute", "")
|
||||
DefNode(GeometryNode, GEO_NODE_POINT_INSTANCE, 0, "POINT_INSTANCE", PointInstance, "Point Instance", "")
|
||||
DefNode(GeometryNode, GEO_NODE_OBJECT_INFO, 0, "OBJECT_INFO", ObjectInfo, "Object Info", "")
|
||||
DefNode(GeometryNode, GEO_NODE_RANDOM_ATTRIBUTE, def_geo_random_attribute, "RANDOM_ATTRIBUTE", RandomAttribute, "Random Attribute", "")
|
||||
|
||||
/* undefine macros */
|
||||
#undef DefNode
|
||||
|
@@ -47,22 +47,12 @@ namespace blender::nodes {
|
||||
|
||||
static Vector<float3> scatter_points_from_mesh(const Mesh *mesh,
|
||||
const float density,
|
||||
const int density_attribute_index)
|
||||
const FloatReadAttribute &density_factors)
|
||||
{
|
||||
/* This only updates a cache and can be considered to be logically const. */
|
||||
const MLoopTri *looptris = BKE_mesh_runtime_looptri_ensure(const_cast<Mesh *>(mesh));
|
||||
const int looptris_len = BKE_mesh_runtime_looptri_len(mesh);
|
||||
|
||||
Array<float> vertex_density_factors(mesh->totvert);
|
||||
if (density_attribute_index == -1) {
|
||||
vertex_density_factors.fill(1.0f);
|
||||
}
|
||||
else {
|
||||
MDeformVert *dverts = mesh->dvert;
|
||||
BKE_defvert_extract_vgroup_to_vertweights(
|
||||
dverts, density_attribute_index, mesh->totvert, vertex_density_factors.data(), false);
|
||||
}
|
||||
|
||||
Vector<float3> points;
|
||||
|
||||
for (const int looptri_index : IndexRange(looptris_len)) {
|
||||
@@ -73,9 +63,9 @@ static Vector<float3> scatter_points_from_mesh(const Mesh *mesh,
|
||||
const float3 v0_pos = mesh->mvert[v0_index].co;
|
||||
const float3 v1_pos = mesh->mvert[v1_index].co;
|
||||
const float3 v2_pos = mesh->mvert[v2_index].co;
|
||||
const float v0_density_factor = vertex_density_factors[v0_index];
|
||||
const float v1_density_factor = vertex_density_factors[v1_index];
|
||||
const float v2_density_factor = vertex_density_factors[v2_index];
|
||||
const float v0_density_factor = density_factors[v0_index];
|
||||
const float v1_density_factor = density_factors[v1_index];
|
||||
const float v2_density_factor = density_factors[v2_index];
|
||||
const float looptri_density_factor = (v0_density_factor + v1_density_factor +
|
||||
v2_density_factor) /
|
||||
3.0f;
|
||||
@@ -121,8 +111,11 @@ static void geo_point_distribute_exec(GeoNodeExecParams params)
|
||||
|
||||
const MeshComponent &mesh_component = *geometry_set.get_component_for_read<MeshComponent>();
|
||||
const Mesh *mesh_in = mesh_component.get_for_read();
|
||||
const int density_attribute_index = mesh_component.vertex_group_index(density_attribute);
|
||||
Vector<float3> points = scatter_points_from_mesh(mesh_in, density, density_attribute_index);
|
||||
|
||||
const FloatReadAttribute density_factors = bke::mesh_attribute_get_for_read<float>(
|
||||
mesh_component, density_attribute, ATTR_DOMAIN_VERTEX, 1.0f);
|
||||
|
||||
Vector<float3> points = scatter_points_from_mesh(mesh_in, density, density_factors);
|
||||
|
||||
PointCloud *pointcloud = BKE_pointcloud_new_nomain(points.size());
|
||||
memcpy(pointcloud->co, points.data(), sizeof(float3) * points.size());
|
||||
|
@@ -38,15 +38,17 @@ static void geo_point_instance_exec(GeoNodeExecParams params)
|
||||
{
|
||||
GeometrySet geometry_set = params.extract_input<GeometrySet>("Geometry");
|
||||
|
||||
Vector<float3> positions;
|
||||
Vector<float3> instance_positions;
|
||||
if (geometry_set.has_pointcloud()) {
|
||||
const PointCloud *pointcloud = geometry_set.get_pointcloud_for_read();
|
||||
positions.extend((const float3 *)pointcloud->co, pointcloud->totpoint);
|
||||
instance_positions.extend((const float3 *)pointcloud->co, pointcloud->totpoint);
|
||||
}
|
||||
if (geometry_set.has_mesh()) {
|
||||
const Mesh *mesh = geometry_set.get_mesh_for_read();
|
||||
for (const int i : IndexRange(mesh->totvert)) {
|
||||
positions.append(mesh->mvert[i].co);
|
||||
const MeshComponent &mesh_component = *geometry_set.get_component_for_read<MeshComponent>();
|
||||
Float3ReadAttribute positions = bke::mesh_attribute_get_for_read<float3>(
|
||||
mesh_component, "Position", ATTR_DOMAIN_VERTEX, {0, 0, 0});
|
||||
for (const int i : IndexRange(positions.size())) {
|
||||
instance_positions.append(positions[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +57,7 @@ static void geo_point_instance_exec(GeoNodeExecParams params)
|
||||
Object *object = params.handle_map().lookup(object_handle);
|
||||
|
||||
InstancesComponent &instances = geometry_set.get_component_for_write<InstancesComponent>();
|
||||
instances.replace(std::move(positions), object);
|
||||
instances.replace(std::move(instance_positions), object);
|
||||
|
||||
params.set_output("Geometry", std::move(geometry_set));
|
||||
}
|
||||
|
110
source/blender/nodes/geometry/nodes/node_geo_random_attribute.cc
Normal file
110
source/blender/nodes/geometry/nodes/node_geo_random_attribute.cc
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "node_geometry_util.hh"
|
||||
|
||||
#include "BLI_rand.hh"
|
||||
|
||||
#include "DNA_mesh_types.h"
|
||||
|
||||
static bNodeSocketTemplate geo_node_random_attribute_in[] = {
|
||||
{SOCK_GEOMETRY, N_("Geometry")},
|
||||
{SOCK_STRING, N_("Attribute")},
|
||||
{SOCK_VECTOR, N_("Min"), 0.0f, 0.0f, 0.0f, 0.0f, -FLT_MAX, FLT_MAX},
|
||||
{SOCK_VECTOR, N_("Max"), 1.0f, 1.0f, 1.0f, 0.0f, -FLT_MAX, FLT_MAX},
|
||||
{SOCK_INT, N_("Seed"), 0, 0, 0, 0, -10000, 10000},
|
||||
{-1, ""},
|
||||
};
|
||||
|
||||
static bNodeSocketTemplate geo_node_random_attribute_out[] = {
|
||||
{SOCK_GEOMETRY, N_("Geometry")},
|
||||
{-1, ""},
|
||||
};
|
||||
|
||||
static void geo_attribute_random_init(bNodeTree *UNUSED(tree), bNode *node)
|
||||
{
|
||||
node->custom1 = CD_PROP_FLOAT;
|
||||
}
|
||||
|
||||
namespace blender::nodes {
|
||||
|
||||
static void geo_random_attribute_exec(GeoNodeExecParams params)
|
||||
{
|
||||
const bNode &node = params.node();
|
||||
const int data_type = node.custom1;
|
||||
const AttributeDomain domain = static_cast<AttributeDomain>(node.custom2);
|
||||
|
||||
GeometrySet geometry_set = params.extract_input<GeometrySet>("Geometry");
|
||||
const std::string attribute_name = params.extract_input<std::string>("Attribute");
|
||||
const float3 min_value = params.extract_input<float3>("Min");
|
||||
const float3 max_value = params.extract_input<float3>("Max");
|
||||
const int seed = params.extract_input<int>("Seed");
|
||||
|
||||
MeshComponent &mesh_component = geometry_set.get_component_for_write<MeshComponent>();
|
||||
Mesh *mesh = mesh_component.get_for_write();
|
||||
if (mesh == nullptr) {
|
||||
params.set_output("Geometry", geometry_set);
|
||||
return;
|
||||
}
|
||||
|
||||
std::optional<WriteAttributePtr> attribute_opt = bke::mesh_attribute_get_for_write(
|
||||
mesh_component, attribute_name);
|
||||
|
||||
if (!attribute_opt.has_value()) {
|
||||
BKE_id_attribute_new(&mesh->id, attribute_name.c_str(), data_type, domain, nullptr);
|
||||
attribute_opt = bke::mesh_attribute_get_for_write(mesh_component, attribute_name);
|
||||
}
|
||||
|
||||
RandomNumberGenerator rng;
|
||||
rng.seed_random(seed);
|
||||
|
||||
if (attribute_opt.has_value()) {
|
||||
WriteAttributePtr attribute = std::move(*attribute_opt);
|
||||
const int size = attribute->size();
|
||||
if (attribute->cpp_type().is<float>()) {
|
||||
FloatWriteAttribute float_attribute = std::move(attribute);
|
||||
for (const int i : IndexRange(size)) {
|
||||
const float value = rng.get_float() * (max_value.x - min_value.x) + min_value.x;
|
||||
float_attribute.set(i, value);
|
||||
}
|
||||
}
|
||||
else if (attribute->cpp_type().is<float3>()) {
|
||||
Float3WriteAttribute float3_attribute = std::move(attribute);
|
||||
for (const int i : IndexRange(size)) {
|
||||
const float x = rng.get_float();
|
||||
const float y = rng.get_float();
|
||||
const float z = rng.get_float();
|
||||
const float3 value = float3(x, y, z) * (max_value - min_value) + min_value;
|
||||
float3_attribute.set(i, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params.set_output("Geometry", geometry_set);
|
||||
}
|
||||
|
||||
} // namespace blender::nodes
|
||||
|
||||
void register_node_type_geo_random_attribute()
|
||||
{
|
||||
static bNodeType ntype;
|
||||
|
||||
geo_node_type_base(&ntype, GEO_NODE_RANDOM_ATTRIBUTE, "Random Attribute", 0, 0);
|
||||
node_type_socket_templates(&ntype, geo_node_random_attribute_in, geo_node_random_attribute_out);
|
||||
node_type_init(&ntype, geo_attribute_random_init);
|
||||
ntype.geometry_node_execute = blender::nodes::geo_random_attribute_exec;
|
||||
nodeRegisterType(&ntype);
|
||||
}
|
Reference in New Issue
Block a user