WIP VoxelGeneratorGraph, compiled and untested
parent
e3e24082af
commit
515751a3e6
1
SCsub
1
SCsub
|
@ -12,6 +12,7 @@ files = [
|
|||
"meshers/*.cpp",
|
||||
"streams/*.cpp",
|
||||
"generators/*.cpp",
|
||||
"generators/graph/*.cpp",
|
||||
"util/*.cpp",
|
||||
"terrain/*.cpp",
|
||||
"math/*.cpp",
|
||||
|
|
|
@ -38,8 +38,7 @@ def get_doc_classes():
|
|||
"VoxelMesherBlocky",
|
||||
"VoxelMesherTransvoxel",
|
||||
"VoxelMesherDMC"
|
||||
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
def get_doc_path():
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
#include "program_graph.h"
|
||||
#include <core/os/file_access.h>
|
||||
#include <core/variant.h>
|
||||
#include <unordered_set>
|
||||
|
||||
template <typename T>
|
||||
inline bool range_contains(const std::vector<T> &vec, const T &v, uint32_t begin, uint32_t end) {
|
||||
CRASH_COND(end > vec.size());
|
||||
for (size_t i = begin; i < end; ++i) {
|
||||
if (vec[i] == v) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t ProgramGraph::Node::find_input_connection(PortLocation src, uint32_t input_port_index) const {
|
||||
CRASH_COND(input_port_index >= inputs.size());
|
||||
const Port &p = inputs[input_port_index];
|
||||
for (size_t i = 0; i < p.connections.size(); ++i) {
|
||||
if (p.connections[i] == src) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return ProgramGraph::NULL_INDEX;
|
||||
}
|
||||
|
||||
uint32_t ProgramGraph::Node::find_output_connection(uint32_t output_port_index, PortLocation dst) const {
|
||||
CRASH_COND(output_port_index >= outputs.size());
|
||||
const Port &p = outputs[output_port_index];
|
||||
for (size_t i = 0; i < p.connections.size(); ++i) {
|
||||
if (p.connections[i] == dst) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return ProgramGraph::NULL_INDEX;
|
||||
}
|
||||
|
||||
ProgramGraph::Node *ProgramGraph::create_node() {
|
||||
Node *node = memnew(Node);
|
||||
node->id = _next_node_id++;
|
||||
_nodes[node->id] = node;
|
||||
return node;
|
||||
}
|
||||
|
||||
void ProgramGraph::remove_node(uint32_t node_id) {
|
||||
Node *node = get_node(node_id);
|
||||
|
||||
// Remove input connections
|
||||
for (uint32_t dst_port_index = 0; dst_port_index < node->inputs.size(); ++dst_port_index) {
|
||||
const Port &p = node->inputs[dst_port_index];
|
||||
for (auto it = p.connections.begin(); it != p.connections.end(); ++it) {
|
||||
const PortLocation src = *it;
|
||||
Node *src_node = get_node(src.node_id);
|
||||
uint32_t i = src_node->find_output_connection(src.port_index, PortLocation{ node_id, dst_port_index });
|
||||
CRASH_COND(i == NULL_INDEX);
|
||||
std::vector<PortLocation> &connections = src_node->outputs[src.port_index].connections;
|
||||
connections.erase(connections.begin() + i);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove output connections
|
||||
for (uint32_t src_port_index = 0; src_port_index < node->outputs.size(); ++src_port_index) {
|
||||
const Port &p = node->outputs[src_port_index];
|
||||
for (auto it = p.connections.begin(); it != p.connections.end(); ++it) {
|
||||
const PortLocation dst = *it;
|
||||
Node *dst_node = get_node(dst.node_id);
|
||||
uint32_t i = dst_node->find_input_connection(PortLocation{ node_id, src_port_index }, dst.port_index);
|
||||
CRASH_COND(i == NULL_INDEX);
|
||||
std::vector<PortLocation> &connections = dst_node->inputs[dst.port_index].connections;
|
||||
connections.erase(connections.begin() + i);
|
||||
}
|
||||
}
|
||||
|
||||
_nodes.erase(node_id);
|
||||
memdelete(node);
|
||||
}
|
||||
|
||||
void ProgramGraph::clear() {
|
||||
for (auto it = _nodes.begin(); it != _nodes.end(); ++it) {
|
||||
Node *node = it->second;
|
||||
CRASH_COND(node == nullptr);
|
||||
memdelete(node);
|
||||
}
|
||||
_nodes.clear();
|
||||
}
|
||||
|
||||
bool ProgramGraph::is_connected(PortLocation src, PortLocation dst) const {
|
||||
Node *src_node = get_node(src.node_id);
|
||||
Node *dst_node = get_node(dst.node_id);
|
||||
if (src_node->find_output_connection(src.port_index, dst) != NULL_INDEX) {
|
||||
CRASH_COND(dst_node->find_input_connection(src, dst.port_index) == NULL_INDEX);
|
||||
return true;
|
||||
} else {
|
||||
CRASH_COND(dst_node->find_input_connection(src, dst.port_index) != NULL_INDEX);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramGraph::connect(PortLocation src, PortLocation dst) {
|
||||
CRASH_COND(is_connected(src, dst));
|
||||
CRASH_COND(has_path(dst.node_id, src.node_id));
|
||||
Node *src_node = get_node(src.node_id);
|
||||
Node *dst_node = get_node(dst.node_id);
|
||||
CRASH_COND(dst_node->inputs[dst.port_index].connections.size() != 0);
|
||||
src_node->outputs[src.port_index].connections.push_back(dst);
|
||||
dst_node->inputs[dst.port_index].connections.push_back(src);
|
||||
}
|
||||
|
||||
bool ProgramGraph::disconnect(PortLocation src, PortLocation dst) {
|
||||
Node *src_node = get_node(src.node_id);
|
||||
Node *dst_node = get_node(dst.node_id);
|
||||
uint32_t src_i = src_node->find_output_connection(src.port_index, dst);
|
||||
if (src_i == NULL_INDEX) {
|
||||
return false;
|
||||
}
|
||||
uint32_t dst_i = dst_node->find_input_connection(src, dst.port_index);
|
||||
CRASH_COND(dst_i == NULL_INDEX);
|
||||
std::vector<PortLocation> &src_connections = src_node->outputs[src.port_index].connections;
|
||||
std::vector<PortLocation> &dst_connections = dst_node->inputs[dst.port_index].connections;
|
||||
src_connections.erase(src_connections.begin() + src_i);
|
||||
dst_connections.erase(dst_connections.begin() + dst_i);
|
||||
return true;
|
||||
}
|
||||
|
||||
ProgramGraph::Node *ProgramGraph::get_node(uint32_t id) const {
|
||||
auto it = _nodes.find(id);
|
||||
CRASH_COND(it == _nodes.end());
|
||||
Node *node = it->second;
|
||||
CRASH_COND(node == nullptr);
|
||||
return node;
|
||||
}
|
||||
|
||||
bool ProgramGraph::has_path(uint32_t p_src_node_id, uint32_t p_dst_node_id) const {
|
||||
std::vector<uint32_t> nodes_to_process;
|
||||
std::unordered_set<uint32_t> visited_nodes;
|
||||
|
||||
nodes_to_process.push_back(p_src_node_id);
|
||||
|
||||
while (nodes_to_process.size() > 0) {
|
||||
const Node *node = get_node(nodes_to_process.back());
|
||||
nodes_to_process.pop_back();
|
||||
visited_nodes.insert(node->id);
|
||||
|
||||
uint32_t nodes_to_process_begin = nodes_to_process.size();
|
||||
|
||||
// Find destinations
|
||||
for (uint32_t oi = 0; oi < node->outputs.size(); ++oi) {
|
||||
const Port &p = node->outputs[oi];
|
||||
for (auto cit = p.connections.begin(); cit != p.connections.end(); ++cit) {
|
||||
PortLocation dst = *cit;
|
||||
if (dst.node_id == p_dst_node_id) {
|
||||
return true;
|
||||
}
|
||||
// A node can have two connections to the same destination node
|
||||
if (range_contains(nodes_to_process, dst.node_id, nodes_to_process_begin, nodes_to_process.size())) {
|
||||
continue;
|
||||
}
|
||||
if (visited_nodes.find(dst.node_id) != visited_nodes.end()) {
|
||||
continue;
|
||||
}
|
||||
nodes_to_process.push_back(dst.node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ProgramGraph::evaluate(std::vector<uint32_t> &order) const {
|
||||
std::vector<uint32_t> nodes_to_process;
|
||||
std::unordered_set<uint32_t> visited_nodes;
|
||||
|
||||
// Find terminal nodes
|
||||
for (auto it = _nodes.begin(); it != _nodes.end(); ++it) {
|
||||
const Node *node = it->second;
|
||||
if (node->outputs.size() == 0) {
|
||||
nodes_to_process.push_back(it->first);
|
||||
}
|
||||
}
|
||||
|
||||
while (nodes_to_process.size() > 0) {
|
||||
const Node *node = get_node(nodes_to_process.back());
|
||||
uint32_t nodes_to_process_begin = nodes_to_process.size();
|
||||
|
||||
// Find ancestors
|
||||
for (uint32_t ii = 0; ii < node->inputs.size(); ++ii) {
|
||||
const Port &p = node->inputs[ii];
|
||||
for (auto cit = p.connections.begin(); cit != p.connections.end(); ++cit) {
|
||||
PortLocation src = *cit;
|
||||
// A node can have two connections to the same destination node
|
||||
if (range_contains(nodes_to_process, src.node_id, nodes_to_process_begin, nodes_to_process.size())) {
|
||||
continue;
|
||||
}
|
||||
if (visited_nodes.find(src.node_id) != visited_nodes.end()) {
|
||||
continue;
|
||||
}
|
||||
nodes_to_process.push_back(src.node_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes_to_process_begin == nodes_to_process.size()) {
|
||||
// No ancestor to visit, process the node
|
||||
order.push_back(node->id);
|
||||
visited_nodes.insert(node->id);
|
||||
nodes_to_process.pop_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramGraph::debug_print_dot_file(String file_path) const {
|
||||
// https://www.graphviz.org/pdf/dotguide.pdf
|
||||
|
||||
Error err;
|
||||
FileAccess *f = FileAccess::open(file_path, FileAccess::WRITE, &err);
|
||||
if (f == nullptr) {
|
||||
ERR_PRINT(String("Could not write ProgramGraph debug file as {0}: error {1}").format(varray(file_path, err)));
|
||||
return;
|
||||
}
|
||||
|
||||
f->store_line("digraph G {");
|
||||
|
||||
for (auto nit = _nodes.begin(); nit != _nodes.end(); ++nit) {
|
||||
const Node *node = nit->second;
|
||||
const uint32_t node_id = nit->first;
|
||||
|
||||
for (auto oit = node->outputs.begin(); oit != node->outputs.end(); ++oit) {
|
||||
const Port &port = *oit;
|
||||
|
||||
for (auto cit = port.connections.begin(); cit != port.connections.end(); ++cit) {
|
||||
PortLocation dst = *cit;
|
||||
f->store_line(String("\tn{0} -> n{1};").format(varray(node_id, dst.node_id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f->store_line("}");
|
||||
|
||||
f->close();
|
||||
memdelete(f);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
#ifndef PROGRAM_GRAPH_H
|
||||
#define PROGRAM_GRAPH_H
|
||||
|
||||
#include <core/hashfuncs.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class ProgramGraph {
|
||||
public:
|
||||
static const uint32_t NULL_ID = 0;
|
||||
static const uint32_t NULL_INDEX = -1;
|
||||
|
||||
struct PortLocation {
|
||||
uint32_t node_id;
|
||||
uint32_t port_index;
|
||||
};
|
||||
|
||||
struct PortLocationHasher {
|
||||
static inline uint32_t hash(const PortLocation &v) {
|
||||
uint32_t hash = hash_djb2_one_32(v.node_id);
|
||||
return hash_djb2_one_32(v.node_id, hash);
|
||||
}
|
||||
};
|
||||
|
||||
struct Port {
|
||||
std::vector<PortLocation> connections;
|
||||
};
|
||||
|
||||
struct Node {
|
||||
uint32_t id;
|
||||
std::vector<Port> inputs;
|
||||
std::vector<Port> outputs;
|
||||
|
||||
uint32_t find_input_connection(PortLocation src, uint32_t input_port_index) const;
|
||||
uint32_t find_output_connection(uint32_t output_port_index, PortLocation dst) const;
|
||||
};
|
||||
|
||||
Node *create_node();
|
||||
Node *get_node(uint32_t id) const;
|
||||
void remove_node(uint32_t id);
|
||||
void clear();
|
||||
|
||||
bool is_connected(PortLocation src, PortLocation dst) const;
|
||||
void connect(PortLocation src, PortLocation dst);
|
||||
bool disconnect(PortLocation src, PortLocation dst);
|
||||
|
||||
bool has_path(uint32_t p_src_node_id, uint32_t p_dst_node_id) const;
|
||||
void evaluate(std::vector<uint32_t> &order) const;
|
||||
|
||||
void debug_print_dot_file(String file_path) const;
|
||||
|
||||
private:
|
||||
std::unordered_map<uint32_t, Node *> _nodes;
|
||||
uint32_t _next_node_id = 1;
|
||||
};
|
||||
|
||||
inline bool operator==(const ProgramGraph::PortLocation &a, const ProgramGraph::PortLocation &b) {
|
||||
return a.node_id == b.node_id && a.port_index == b.port_index;
|
||||
}
|
||||
|
||||
#endif // PROGRAM_GRAPH_H
|
|
@ -0,0 +1,659 @@
|
|||
#include "voxel_generator_graph.h"
|
||||
|
||||
namespace {
|
||||
VoxelGeneratorGraph::NodeTypeDB *g_node_type_db = nullptr;
|
||||
}
|
||||
|
||||
VoxelGeneratorGraph::NodeTypeDB *VoxelGeneratorGraph::NodeTypeDB::get_singleton() {
|
||||
CRASH_COND(g_node_type_db == nullptr);
|
||||
return g_node_type_db;
|
||||
}
|
||||
|
||||
void VoxelGeneratorGraph::NodeTypeDB::create_singleton() {
|
||||
CRASH_COND(g_node_type_db != nullptr);
|
||||
g_node_type_db = memnew(NodeTypeDB());
|
||||
}
|
||||
|
||||
void VoxelGeneratorGraph::NodeTypeDB::destroy_singleton() {
|
||||
CRASH_COND(g_node_type_db == nullptr);
|
||||
memdelete(g_node_type_db);
|
||||
g_node_type_db = nullptr;
|
||||
}
|
||||
|
||||
VoxelGeneratorGraph::NodeTypeDB::NodeTypeDB() {
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_CONSTANT];
|
||||
t.outputs.push_back(Port("Value", true));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_INPUT_X];
|
||||
t.outputs.push_back(Port("Value", true));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_INPUT_Y];
|
||||
t.outputs.push_back(Port("Value", true));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_INPUT_Z];
|
||||
t.outputs.push_back(Port("Value", true));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_OUTPUT_SDF];
|
||||
t.inputs.push_back(Port("Value", true));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_ADD];
|
||||
t.inputs.push_back(Port("A"));
|
||||
t.inputs.push_back(Port("B"));
|
||||
t.outputs.push_back(Port("Sum"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_SUBTRACT];
|
||||
t.inputs.push_back(Port("A"));
|
||||
t.inputs.push_back(Port("B"));
|
||||
t.outputs.push_back(Port("Sum"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_MULTIPLY];
|
||||
t.inputs.push_back(Port("A"));
|
||||
t.inputs.push_back(Port("B"));
|
||||
t.outputs.push_back(Port("Product"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_SINE];
|
||||
t.inputs.push_back(Port("X"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_FLOOR];
|
||||
t.inputs.push_back(Port("X"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_ABS];
|
||||
t.inputs.push_back(Port("X"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_SQRT];
|
||||
t.inputs.push_back(Port("X"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_DISTANCE_2D];
|
||||
t.inputs.push_back(Port("X0"));
|
||||
t.inputs.push_back(Port("Y0"));
|
||||
t.inputs.push_back(Port("X1"));
|
||||
t.inputs.push_back(Port("Y1"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_DISTANCE_3D];
|
||||
t.inputs.push_back(Port("X0"));
|
||||
t.inputs.push_back(Port("Y0"));
|
||||
t.inputs.push_back(Port("Y0"));
|
||||
t.inputs.push_back(Port("X1"));
|
||||
t.inputs.push_back(Port("Y1"));
|
||||
t.inputs.push_back(Port("Z1"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_CLAMP];
|
||||
t.inputs.push_back(Port("X"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
t.params.push_back(Param("Min", -1.0));
|
||||
t.params.push_back(Param("Max", 1.0));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_MIX];
|
||||
t.inputs.push_back(Port("A"));
|
||||
t.inputs.push_back(Port("B"));
|
||||
t.inputs.push_back(Port("Ratio"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_REMAP];
|
||||
t.inputs.push_back(Port("X"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
t.params.push_back(Param("Min0", -1.0));
|
||||
t.params.push_back(Param("Max0", 1.0));
|
||||
t.params.push_back(Param("Min1", -1.0));
|
||||
t.params.push_back(Param("Max1", 1.0));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_CURVE];
|
||||
t.inputs.push_back(Port("X"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
t.params.push_back(Param("Curve"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_NOISE_2D];
|
||||
t.inputs.push_back(Port("X"));
|
||||
t.inputs.push_back(Port("Y"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
t.params.push_back(Param("Noise"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_NOISE_3D];
|
||||
t.inputs.push_back(Port("X"));
|
||||
t.inputs.push_back(Port("Y"));
|
||||
t.inputs.push_back(Port("Z"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
t.params.push_back(Param("Noise"));
|
||||
}
|
||||
{
|
||||
NodeType &t = types[VoxelGeneratorGraph::NODE_IMAGE_2D];
|
||||
t.inputs.push_back(Port("X"));
|
||||
t.inputs.push_back(Port("Y"));
|
||||
t.outputs.push_back(Port("Result"));
|
||||
t.params.push_back(Param("Image"));
|
||||
}
|
||||
}
|
||||
|
||||
VoxelGeneratorGraph::VoxelGeneratorGraph() {
|
||||
|
||||
typedef ProgramGraph::PortLocation PL;
|
||||
|
||||
// Default
|
||||
uint32_t n_x = create_node(NODE_INPUT_X);
|
||||
uint32_t n_y = create_node(NODE_INPUT_Y);
|
||||
uint32_t n_z = create_node(NODE_INPUT_Y);
|
||||
uint32_t n_o = create_node(NODE_OUTPUT_SDF);
|
||||
uint32_t n_sin0 = create_node(NODE_SINE);
|
||||
uint32_t n_sin1 = create_node(NODE_SINE);
|
||||
uint32_t n_add = create_node(NODE_ADD);
|
||||
uint32_t n_mul0 = create_node(NODE_MULTIPLY);
|
||||
uint32_t n_mul1 = create_node(NODE_MULTIPLY);
|
||||
uint32_t n_mul2 = create_node(NODE_MULTIPLY);
|
||||
uint32_t n_c0 = create_node(NODE_CONSTANT);
|
||||
uint32_t n_c1 = create_node(NODE_CONSTANT);
|
||||
uint32_t n_sub = create_node(NODE_SUBTRACT);
|
||||
|
||||
node_set_param(n_c0, 0, 1.0 / 20.0);
|
||||
node_set_param(n_c1, 0, 10.0);
|
||||
|
||||
/*
|
||||
* X --- * --- sin Z
|
||||
* / \ \
|
||||
* 1/20 + --- * --- - --- O
|
||||
* \ / /
|
||||
* Y --- * --- sin 10.0
|
||||
*/
|
||||
|
||||
node_connect(PL{ n_x, 0 }, PL{ n_mul0, 0 });
|
||||
node_connect(PL{ n_y, 0 }, PL{ n_mul1, 0 });
|
||||
node_connect(PL{ n_c0, 0 }, PL{ n_mul0, 1 });
|
||||
node_connect(PL{ n_c0, 0 }, PL{ n_mul1, 1 });
|
||||
node_connect(PL{ n_mul0, 0 }, PL{ n_sin0, 0 });
|
||||
node_connect(PL{ n_mul1, 0 }, PL{ n_sin1, 0 });
|
||||
node_connect(PL{ n_sin0, 0 }, PL{ n_add, 0 });
|
||||
node_connect(PL{ n_sin1, 0 }, PL{ n_add, 1 });
|
||||
node_connect(PL{ n_add, 0 }, PL{ n_mul2, 0 });
|
||||
node_connect(PL{ n_c1, 0 }, PL{ n_mul2, 1 });
|
||||
node_connect(PL{ n_z, 0 }, PL{ n_sub, 0 });
|
||||
node_connect(PL{ n_mul2, 0 }, PL{ n_sub, 1 });
|
||||
node_connect(PL{ n_sub, 0 }, PL{ n_o, 0 });
|
||||
|
||||
_graph.debug_print_dot_file("voxel_graph_test.dot");
|
||||
}
|
||||
|
||||
VoxelGeneratorGraph::~VoxelGeneratorGraph() {
|
||||
clear();
|
||||
}
|
||||
|
||||
void VoxelGeneratorGraph::clear() {
|
||||
const uint32_t *key;
|
||||
while ((key = _nodes.next(key))) {
|
||||
Node *node = _nodes.get(*key);
|
||||
CRASH_COND(node == nullptr);
|
||||
memdelete(node);
|
||||
}
|
||||
_nodes.clear();
|
||||
_graph.clear();
|
||||
|
||||
_program.clear();
|
||||
_memory.clear();
|
||||
}
|
||||
|
||||
uint32_t VoxelGeneratorGraph::create_node(NodeTypeID type_id) {
|
||||
const NodeTypeDB::NodeType &type = NodeTypeDB::get_singleton()->types[type_id];
|
||||
|
||||
ProgramGraph::Node *pg_node = _graph.create_node();
|
||||
pg_node->inputs.resize(type.inputs.size());
|
||||
pg_node->outputs.resize(type.outputs.size());
|
||||
|
||||
Node *node = memnew(Node);
|
||||
node->params.resize(type.params.size());
|
||||
node->type = type_id;
|
||||
_nodes[pg_node->id] = node;
|
||||
|
||||
return pg_node->id;
|
||||
}
|
||||
|
||||
void VoxelGeneratorGraph::remove_node(uint32_t node_id) {
|
||||
_graph.remove_node(node_id);
|
||||
|
||||
Node **pptr = _nodes.getptr(node_id);
|
||||
if (pptr != nullptr) {
|
||||
Node *node = *pptr;
|
||||
memdelete(node);
|
||||
_nodes.erase(node_id);
|
||||
}
|
||||
}
|
||||
|
||||
void VoxelGeneratorGraph::node_connect(ProgramGraph::PortLocation src, ProgramGraph::PortLocation dst) {
|
||||
_graph.connect(src, dst);
|
||||
}
|
||||
|
||||
void VoxelGeneratorGraph::node_set_param(uint32_t node_id, uint32_t param_index, Variant value) {
|
||||
Node **pptr = _nodes.getptr(node_id);
|
||||
ERR_FAIL_COND(pptr == nullptr);
|
||||
Node *node = *pptr;
|
||||
node->params[param_index] = value;
|
||||
}
|
||||
|
||||
void VoxelGeneratorGraph::generate_block(VoxelBlockRequest &input) {
|
||||
|
||||
VoxelBuffer &out_buffer = **input.voxel_buffer;
|
||||
|
||||
const Vector3i bs = out_buffer.get_size();
|
||||
const VoxelBuffer::ChannelId channel = _channel;
|
||||
|
||||
Vector3i rpos;
|
||||
Vector3i gpos;
|
||||
// Loads of possible optimization from there
|
||||
|
||||
int stride = 1 << input.lod;
|
||||
for (rpos.z = 0, gpos.z = input.origin_in_voxels.z; rpos.z < bs.z; ++rpos.z, gpos.z += stride) {
|
||||
for (rpos.x = 0, gpos.x = input.origin_in_voxels.x; rpos.x < bs.x; ++rpos.x, gpos.x += stride) {
|
||||
for (rpos.y = 0, gpos.y = input.origin_in_voxels.y; rpos.y < bs.y; ++rpos.y, gpos.y += stride) {
|
||||
|
||||
out_buffer.set_voxel_f(generate_single(gpos), rpos.x, rpos.y, rpos.z, channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out_buffer.compress_uniform_channels();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline void write_static(std::vector<uint8_t> &mem, uint32_t p, const T &v) {
|
||||
CRASH_COND(p + sizeof(T) >= mem.size());
|
||||
*(T *)(&mem[p]) = v;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline void append(std::vector<uint8_t> &mem, const T &v) {
|
||||
size_t p = mem.size();
|
||||
mem.resize(p + sizeof(T));
|
||||
*(T *)(&mem[p]) = v;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline void write_op(std::vector<uint8_t> &mem, uint8_t opid, T data) {
|
||||
uint32_t p = mem.size();
|
||||
mem.resize(p + 1 + sizeof(T));
|
||||
mem[p++] = opid;
|
||||
write_static(mem, p, data);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline const T &read(const std::vector<uint8_t> &mem, uint32_t &p) {
|
||||
CRASH_COND(p + sizeof(T) >= mem.size());
|
||||
const T *v = (const T *)&mem[p];
|
||||
p += sizeof(T);
|
||||
return *v;
|
||||
}
|
||||
|
||||
inline float get_pixel_repeat(Image &im, int x, int y) {
|
||||
return im.get_pixel(wrap(x, im.get_width()), wrap(y, im.get_height())).r;
|
||||
}
|
||||
|
||||
inline float squared(float x) {
|
||||
return x * x;
|
||||
}
|
||||
|
||||
void VoxelGeneratorGraph::compile() {
|
||||
std::vector<uint32_t> order;
|
||||
_graph.evaluate(order);
|
||||
|
||||
_program.clear();
|
||||
|
||||
// Main inputs X, Y, Z
|
||||
_memory.resize(3);
|
||||
|
||||
std::vector<uint8_t> &program = _program;
|
||||
const NodeTypeDB &type_db = *NodeTypeDB::get_singleton();
|
||||
HashMap<ProgramGraph::PortLocation, uint16_t, ProgramGraph::PortLocationHasher> output_port_addresses;
|
||||
bool has_output = false;
|
||||
|
||||
for (size_t i = 0; i < order.size(); ++i) {
|
||||
const uint32_t node_id = order[i];
|
||||
const ProgramGraph::Node *pg_node = _graph.get_node(node_id);
|
||||
const Node *node = _nodes[node_id];
|
||||
const NodeTypeDB::NodeType &type = type_db.types[node->type];
|
||||
|
||||
CRASH_COND(node == nullptr);
|
||||
CRASH_COND(pg_node->inputs.size() != type.inputs.size());
|
||||
CRASH_COND(pg_node->outputs.size() != type.outputs.size());
|
||||
|
||||
switch (node->type) {
|
||||
case NODE_CONSTANT: {
|
||||
CRASH_COND(type.outputs.size() != 1);
|
||||
CRASH_COND(type.params.size() != 1);
|
||||
uint16_t a = _memory.size();
|
||||
_memory.push_back(node->params[0].operator float());
|
||||
output_port_addresses[ProgramGraph::PortLocation{ node_id, 0 }] = a;
|
||||
} break;
|
||||
|
||||
case NODE_INPUT_X:
|
||||
output_port_addresses[ProgramGraph::PortLocation{ node_id, 0 }] = 0;
|
||||
break;
|
||||
|
||||
case NODE_INPUT_Y:
|
||||
output_port_addresses[ProgramGraph::PortLocation{ node_id, 0 }] = 1;
|
||||
break;
|
||||
|
||||
case NODE_INPUT_Z:
|
||||
output_port_addresses[ProgramGraph::PortLocation{ node_id, 0 }] = 2;
|
||||
break;
|
||||
|
||||
case NODE_OUTPUT_SDF:
|
||||
// TODO Multiple outputs may be supported if we get branching
|
||||
CRASH_COND(has_output);
|
||||
has_output = true;
|
||||
break;
|
||||
|
||||
default: {
|
||||
append(program, static_cast<uint8_t>(node->type));
|
||||
|
||||
// Add inputs
|
||||
for (size_t j = 0; j < type.inputs.size(); ++j) {
|
||||
uint16_t a;
|
||||
|
||||
if (pg_node->inputs[j].connections.size() == 0) {
|
||||
// No input, default it
|
||||
// TODO Take param value if specified
|
||||
a = _memory.size();
|
||||
_memory.push_back(0);
|
||||
|
||||
} else {
|
||||
ProgramGraph::PortLocation src_port = pg_node->inputs[j].connections[0];
|
||||
const uint16_t *aptr = output_port_addresses.getptr(src_port);
|
||||
// Previous node ports must have been registered
|
||||
CRASH_COND(aptr == nullptr);
|
||||
a = *aptr;
|
||||
}
|
||||
|
||||
append(program, a);
|
||||
}
|
||||
|
||||
// Add outputs
|
||||
for (size_t j = 0; j < type.outputs.size(); ++j) {
|
||||
const uint16_t a = _memory.size();
|
||||
_memory.push_back(0);
|
||||
|
||||
// This will be used by next nodes
|
||||
const ProgramGraph::PortLocation op{ node_id, static_cast<uint32_t>(j) };
|
||||
output_port_addresses[op] = a;
|
||||
|
||||
append(program, a);
|
||||
}
|
||||
|
||||
// Add special params
|
||||
switch (node->type) {
|
||||
|
||||
case NODE_CURVE: {
|
||||
Ref<Curve> curve = node->params[0];
|
||||
CRASH_COND(curve.is_null());
|
||||
append(program, *curve);
|
||||
} break;
|
||||
|
||||
case NODE_IMAGE_2D: {
|
||||
Ref<Image> im = node->params[0];
|
||||
CRASH_COND(im.is_null());
|
||||
append(program, *im);
|
||||
} break;
|
||||
|
||||
case NODE_NOISE_2D:
|
||||
case NODE_NOISE_3D: {
|
||||
Ref<OpenSimplexNoise> noise = node->params[0];
|
||||
CRASH_COND(noise.is_null());
|
||||
append(program, *noise);
|
||||
} break;
|
||||
|
||||
// TODO Worth it?
|
||||
case NODE_CLAMP:
|
||||
append(program, node->params[0].operator float());
|
||||
append(program, node->params[1].operator float());
|
||||
break;
|
||||
|
||||
case NODE_REMAP: {
|
||||
float min0 = node->params[0].operator float();
|
||||
float max0 = node->params[1].operator float();
|
||||
float min1 = node->params[2].operator float();
|
||||
float max1 = node->params[3].operator float();
|
||||
append(program, -min0);
|
||||
append(program, Math::is_equal_approx(max0, min0) ? 99999.f : 1.f / (max0 - min0));
|
||||
append(program, min1);
|
||||
append(program, max1 - min1);
|
||||
} break;
|
||||
}
|
||||
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_memory.size() < 4) {
|
||||
// In case there is nothing
|
||||
_memory.resize(4, 0);
|
||||
}
|
||||
|
||||
CRASH_COND(!has_output);
|
||||
}
|
||||
|
||||
// The order of fields in the following structs matters.
|
||||
// Inputs go first, then outputs, then params (if applicable at runtime).
|
||||
|
||||
struct PNodeBinop {
|
||||
uint16_t a_i0;
|
||||
uint16_t a_i1;
|
||||
uint16_t a_out;
|
||||
};
|
||||
|
||||
struct PNodeMonoFunc {
|
||||
uint16_t a_in;
|
||||
uint16_t a_out;
|
||||
};
|
||||
|
||||
struct PNodeDistance2D {
|
||||
uint16_t a_x0;
|
||||
uint16_t a_y0;
|
||||
uint16_t a_x1;
|
||||
uint16_t a_y1;
|
||||
uint16_t a_out;
|
||||
};
|
||||
|
||||
struct PNodeDistance3D {
|
||||
uint16_t a_x0;
|
||||
uint16_t a_y0;
|
||||
uint16_t a_z0;
|
||||
uint16_t a_x1;
|
||||
uint16_t a_y1;
|
||||
uint16_t a_z1;
|
||||
uint16_t a_out;
|
||||
};
|
||||
|
||||
struct PNodeClamp {
|
||||
uint16_t a_x;
|
||||
uint16_t a_out;
|
||||
float p_min;
|
||||
float p_max;
|
||||
};
|
||||
|
||||
struct PNodeMix {
|
||||
uint16_t a_i0;
|
||||
uint16_t a_i1;
|
||||
uint16_t a_ratio;
|
||||
uint16_t a_out;
|
||||
};
|
||||
|
||||
struct PNodeRemap {
|
||||
uint16_t a_x;
|
||||
uint16_t a_out;
|
||||
float p_c0;
|
||||
float p_m0;
|
||||
float p_c1;
|
||||
float p_m1;
|
||||
};
|
||||
|
||||
struct PNodeCurve {
|
||||
uint16_t a_in;
|
||||
uint16_t a_out;
|
||||
Curve *p_curve;
|
||||
};
|
||||
|
||||
struct PNodeNoise2D {
|
||||
uint16_t a_x;
|
||||
uint16_t a_y;
|
||||
uint16_t a_out;
|
||||
OpenSimplexNoise *p_noise;
|
||||
};
|
||||
|
||||
struct PNodeNoise3D {
|
||||
uint16_t a_x;
|
||||
uint16_t a_y;
|
||||
uint16_t a_z;
|
||||
uint16_t a_out;
|
||||
OpenSimplexNoise *p_noise;
|
||||
};
|
||||
|
||||
struct PNodeImage2D {
|
||||
uint16_t a_x;
|
||||
uint16_t a_y;
|
||||
uint16_t a_out;
|
||||
Image *p_image;
|
||||
};
|
||||
|
||||
float VoxelGeneratorGraph::generate_single(const Vector3i &position) {
|
||||
// This part must be optimized for speed
|
||||
|
||||
std::vector<float> &memory = _memory;
|
||||
memory[0] = position.x;
|
||||
memory[1] = position.y;
|
||||
memory[2] = position.z;
|
||||
|
||||
uint32_t pc = 0;
|
||||
while (pc < _program.size()) {
|
||||
|
||||
const uint8_t opid = _program[pc++];
|
||||
|
||||
switch (opid) {
|
||||
case NODE_CONSTANT:
|
||||
case NODE_INPUT_X:
|
||||
case NODE_INPUT_Y:
|
||||
case NODE_INPUT_Z:
|
||||
case NODE_OUTPUT_SDF:
|
||||
// Not part of the runtime
|
||||
CRASH_NOW();
|
||||
break;
|
||||
|
||||
case NODE_ADD: {
|
||||
const PNodeBinop &n = read<PNodeBinop>(_program, pc);
|
||||
memory[n.a_out] = memory[n.a_i0] + memory[n.a_i1];
|
||||
} break;
|
||||
|
||||
case NODE_SUBTRACT: {
|
||||
const PNodeBinop &n = read<PNodeBinop>(_program, pc);
|
||||
memory[n.a_out] = memory[n.a_i0] - memory[n.a_i1];
|
||||
} break;
|
||||
|
||||
case NODE_MULTIPLY: {
|
||||
const PNodeBinop &n = read<PNodeBinop>(_program, pc);
|
||||
memory[n.a_out] = memory[n.a_i0] * memory[n.a_i1];
|
||||
} break;
|
||||
|
||||
case NODE_SINE: {
|
||||
const PNodeMonoFunc &n = read<PNodeMonoFunc>(_program, pc);
|
||||
memory[n.a_out] = Math::sin(Math_PI * memory[n.a_in]);
|
||||
} break;
|
||||
|
||||
case NODE_FLOOR: {
|
||||
const PNodeMonoFunc &n = read<PNodeMonoFunc>(_program, pc);
|
||||
memory[n.a_out] = Math::floor(memory[n.a_in]);
|
||||
} break;
|
||||
|
||||
case NODE_ABS: {
|
||||
const PNodeMonoFunc &n = read<PNodeMonoFunc>(_program, pc);
|
||||
memory[n.a_out] = Math::abs(memory[n.a_in]);
|
||||
} break;
|
||||
|
||||
case NODE_SQRT: {
|
||||
const PNodeMonoFunc &n = read<PNodeMonoFunc>(_program, pc);
|
||||
memory[n.a_out] = Math::sqrt(memory[n.a_in]);
|
||||
} break;
|
||||
|
||||
case NODE_DISTANCE_2D: {
|
||||
const PNodeDistance2D &n = read<PNodeDistance2D>(_program, pc);
|
||||
memory[n.a_out] = Math::sqrt(squared(memory[n.a_x1] - memory[n.a_x0]) +
|
||||
squared(memory[n.a_y1] - memory[n.a_y0]));
|
||||
} break;
|
||||
|
||||
case NODE_DISTANCE_3D: {
|
||||
const PNodeDistance3D &n = read<PNodeDistance3D>(_program, pc);
|
||||
memory[n.a_out] = Math::sqrt(squared(memory[n.a_x1] - memory[n.a_x0]) +
|
||||
squared(memory[n.a_y1] - memory[n.a_y0]) +
|
||||
squared(memory[n.a_z1] - memory[n.a_z0]));
|
||||
} break;
|
||||
|
||||
case NODE_MIX: {
|
||||
const PNodeMix &n = read<PNodeMix>(_program, pc);
|
||||
memory[n.a_out] = Math::lerp(memory[n.a_i0], memory[n.a_i1], memory[n.a_ratio]);
|
||||
} break;
|
||||
|
||||
case NODE_CLAMP: {
|
||||
const PNodeClamp &n = read<PNodeClamp>(_program, pc);
|
||||
memory[n.a_out] = clamp(memory[n.a_x], memory[n.p_min], memory[n.p_max]);
|
||||
} break;
|
||||
|
||||
case NODE_REMAP: {
|
||||
const PNodeRemap &n = read<PNodeRemap>(_program, pc);
|
||||
memory[n.a_out] = ((memory[n.a_x] - n.p_c0) * n.p_m0) * n.p_m1 + n.p_c1;
|
||||
} break;
|
||||
|
||||
case NODE_CURVE: {
|
||||
const PNodeCurve &n = read<PNodeCurve>(_program, pc);
|
||||
memory[n.a_out] = n.p_curve->interpolate_baked(memory[n.a_in]);
|
||||
} break;
|
||||
|
||||
case NODE_NOISE_2D: {
|
||||
const PNodeNoise2D &n = read<PNodeNoise2D>(_program, pc);
|
||||
memory[n.a_out] = n.p_noise->get_noise_2d(memory[n.a_x], memory[n.a_y]);
|
||||
} break;
|
||||
|
||||
case NODE_NOISE_3D: {
|
||||
const PNodeNoise3D &n = read<PNodeNoise3D>(_program, pc);
|
||||
memory[n.a_out] = n.p_noise->get_noise_3d(memory[n.a_x], memory[n.a_y], memory[n.a_z]);
|
||||
} break;
|
||||
|
||||
case NODE_IMAGE_2D: {
|
||||
const PNodeImage2D &n = read<PNodeImage2D>(_program, pc);
|
||||
// TODO Not great, but in Godot 4.0 we won't need to lock anymore. Otherwise, need to do it in a pre-run and post-run
|
||||
n.p_image->lock();
|
||||
memory[n.a_out] = get_pixel_repeat(*n.p_image, memory[n.a_x], memory[n.a_y]);
|
||||
n.p_image->unlock();
|
||||
} break;
|
||||
|
||||
default:
|
||||
CRASH_NOW();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return memory.back() * _iso_scale;
|
||||
}
|
||||
|
||||
void VoxelGeneratorGraph::_bind_methods() {
|
||||
|
||||
ClassDB::bind_method(D_METHOD("clear"), &VoxelGeneratorGraph::clear);
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
#ifndef VOXEL_GENERATOR_GRAPH_H
|
||||
#define VOXEL_GENERATOR_GRAPH_H
|
||||
|
||||
#include "../voxel_generator.h"
|
||||
#include "program_graph.h"
|
||||
#include <modules/opensimplex/open_simplex_noise.h>
|
||||
|
||||
class VoxelGeneratorGraph : public VoxelGenerator {
|
||||
GDCLASS(VoxelGeneratorGraph, VoxelGenerator)
|
||||
public:
|
||||
enum NodeTypeID {
|
||||
NODE_CONSTANT,
|
||||
NODE_INPUT_X,
|
||||
NODE_INPUT_Y,
|
||||
NODE_INPUT_Z,
|
||||
NODE_OUTPUT_SDF,
|
||||
NODE_ADD,
|
||||
NODE_SUBTRACT,
|
||||
NODE_MULTIPLY,
|
||||
NODE_SINE,
|
||||
NODE_FLOOR,
|
||||
NODE_ABS,
|
||||
NODE_SQRT,
|
||||
NODE_DISTANCE_2D,
|
||||
NODE_DISTANCE_3D,
|
||||
NODE_CLAMP,
|
||||
NODE_MIX,
|
||||
NODE_REMAP,
|
||||
NODE_CURVE,
|
||||
NODE_NOISE_2D,
|
||||
NODE_NOISE_3D,
|
||||
NODE_IMAGE_2D,
|
||||
NODE_TYPE_COUNT
|
||||
};
|
||||
|
||||
struct NodeTypeDB {
|
||||
|
||||
struct Port {
|
||||
String name;
|
||||
bool fixed_address;
|
||||
|
||||
Port(String p_name, bool p_fixed_address = false) :
|
||||
name(p_name),
|
||||
fixed_address(p_fixed_address) {}
|
||||
};
|
||||
|
||||
struct Param {
|
||||
String name;
|
||||
float default_value;
|
||||
|
||||
Param(String p_name, float p_default_value = 0) :
|
||||
name(p_name),
|
||||
default_value(p_default_value) {}
|
||||
};
|
||||
|
||||
struct NodeType {
|
||||
std::vector<Port> inputs;
|
||||
std::vector<Port> outputs;
|
||||
std::vector<Param> params;
|
||||
};
|
||||
|
||||
NodeTypeDB();
|
||||
|
||||
static NodeTypeDB *get_singleton();
|
||||
static void create_singleton();
|
||||
static void destroy_singleton();
|
||||
|
||||
FixedArray<NodeType, NODE_TYPE_COUNT> types;
|
||||
};
|
||||
|
||||
VoxelGeneratorGraph();
|
||||
~VoxelGeneratorGraph();
|
||||
|
||||
void clear();
|
||||
|
||||
uint32_t create_node(NodeTypeID type_id);
|
||||
void remove_node(uint32_t node_id);
|
||||
void node_connect(ProgramGraph::PortLocation src, ProgramGraph::PortLocation dst);
|
||||
void node_set_param(uint32_t node_id, uint32_t param_index, Variant value);
|
||||
|
||||
void generate_block(VoxelBlockRequest &input) override;
|
||||
float generate_single(const Vector3i &position);
|
||||
|
||||
private:
|
||||
void compile();
|
||||
|
||||
static void _bind_methods();
|
||||
|
||||
struct Node {
|
||||
NodeTypeID type;
|
||||
std::vector<Variant> params;
|
||||
Vector2 gui_position;
|
||||
};
|
||||
|
||||
ProgramGraph _graph;
|
||||
HashMap<uint32_t, Node *> _nodes;
|
||||
|
||||
std::vector<uint8_t> _program;
|
||||
std::vector<float> _memory;
|
||||
VoxelBuffer::ChannelId _channel = VoxelBuffer::CHANNEL_SDF;
|
||||
float _iso_scale = 0.1;
|
||||
};
|
||||
|
||||
#endif // VOXEL_GENERATOR_GRAPH_H
|
|
@ -1,5 +1,6 @@
|
|||
#include "register_types.h"
|
||||
#include "edition/voxel_tool.h"
|
||||
#include "generators/graph/voxel_generator_graph.h"
|
||||
#include "generators/voxel_generator_flat.h"
|
||||
#include "generators/voxel_generator_heightmap.h"
|
||||
#include "generators/voxel_generator_image.h"
|
||||
|
@ -49,6 +50,7 @@ void register_voxel_types() {
|
|||
ClassDB::register_class<VoxelGeneratorImage>();
|
||||
ClassDB::register_class<VoxelGeneratorNoise2D>();
|
||||
ClassDB::register_class<VoxelGeneratorNoise>();
|
||||
ClassDB::register_class<VoxelGeneratorGraph>();
|
||||
|
||||
// Helpers
|
||||
ClassDB::register_class<VoxelBoxMover>();
|
||||
|
@ -63,6 +65,7 @@ void register_voxel_types() {
|
|||
|
||||
VoxelMemoryPool::create_singleton();
|
||||
VoxelStringNames::create_singleton();
|
||||
VoxelGeneratorGraph::NodeTypeDB::create_singleton();
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
VoxelDebug::create_debug_box_mesh();
|
||||
|
@ -79,6 +82,8 @@ void unregister_voxel_types() {
|
|||
|
||||
VoxelStringNames::destroy_singleton();
|
||||
|
||||
VoxelGeneratorGraph::NodeTypeDB::destroy_singleton();
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
VoxelDebug::free_debug_box_mesh();
|
||||
#endif
|
||||
|
|
Loading…
Reference in New Issue