mirror of
https://github.com/Relintai/godot_voxel.git
synced 2025-05-01 17:57:55 +02:00
Fixed view distance + dynamic load/unload around viewer
This commit is contained in:
parent
488ca3e6fd
commit
115e0e2870
62
rect3i.h
Normal file
62
rect3i.h
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#ifndef RECT3I_H
|
||||||
|
#define RECT3I_H
|
||||||
|
|
||||||
|
#include "vector3i.h"
|
||||||
|
//#include <math_funcs.h>
|
||||||
|
|
||||||
|
class Rect3i {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
Vector3i pos;
|
||||||
|
Vector3i size;
|
||||||
|
|
||||||
|
Rect3i() {}
|
||||||
|
|
||||||
|
Rect3i(Vector3i p_pos, Vector3i p_size) : pos(p_pos), size(p_size) {}
|
||||||
|
|
||||||
|
Rect3i(const Rect3i &other) : pos(other.pos), size(other.size) {}
|
||||||
|
|
||||||
|
static inline Rect3i from_center_extents(Vector3i center, Vector3i extents) {
|
||||||
|
return Rect3i(center - extents, 2*extents);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline Rect3i get_bounding_box(Rect3i a, Rect3i b) {
|
||||||
|
|
||||||
|
Rect3i box;
|
||||||
|
|
||||||
|
box.pos.x = MIN(a.pos.x, b.pos.x);
|
||||||
|
box.pos.y = MIN(a.pos.y, b.pos.y);
|
||||||
|
box.pos.z = MIN(a.pos.z, b.pos.z);
|
||||||
|
|
||||||
|
Vector3i max_a = a.pos + a.size;
|
||||||
|
Vector3i max_b = b.pos + b.size;
|
||||||
|
|
||||||
|
box.size.x = MAX(max_a.x, max_b.x) - box.pos.x;
|
||||||
|
box.size.y = MAX(max_a.y, max_b.y) - box.pos.y;
|
||||||
|
box.size.z = MAX(max_a.z, max_b.z) - box.pos.z;
|
||||||
|
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool inline contains(Vector3i p_pos) const {
|
||||||
|
Vector3i end = pos + size;
|
||||||
|
return p_pos.x >= pos.x
|
||||||
|
&& p_pos.y >= pos.y
|
||||||
|
&& p_pos.z >= pos.z
|
||||||
|
&& p_pos.x < end.x
|
||||||
|
&& p_pos.y < end.y
|
||||||
|
&& p_pos.z < end.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
String to_string() const {
|
||||||
|
return String("(o:{0}, s:{1})").format(varray(pos.to_vec3(), size.to_vec3()));
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool operator!=(const Rect3i & a, const Rect3i & b) {
|
||||||
|
return a.pos != b.pos || a.size != b.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // RECT3I_H
|
@ -18,6 +18,9 @@ struct Vector3i {
|
|||||||
_FORCE_INLINE_ Vector3i()
|
_FORCE_INLINE_ Vector3i()
|
||||||
: x(0), y(0), z(0) {}
|
: x(0), y(0), z(0) {}
|
||||||
|
|
||||||
|
_FORCE_INLINE_ Vector3i(int xyz)
|
||||||
|
: x(xyz), y(xyz), z(xyz) {}
|
||||||
|
|
||||||
_FORCE_INLINE_ Vector3i(int px, int py, int pz)
|
_FORCE_INLINE_ Vector3i(int px, int py, int pz)
|
||||||
: x(px), y(py), z(pz) {}
|
: x(px), y(py), z(pz) {}
|
||||||
|
|
||||||
|
@ -125,8 +125,10 @@ bool VoxelBuffer::is_uniform(unsigned int channel_index) const {
|
|||||||
|
|
||||||
const Channel &channel = _channels[channel_index];
|
const Channel &channel = _channels[channel_index];
|
||||||
if (channel.data == NULL)
|
if (channel.data == NULL)
|
||||||
|
// Channel has been optimized
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
// Channel isn't optimized, so must look at each voxel
|
||||||
uint8_t voxel = channel.data[0];
|
uint8_t voxel = channel.data[0];
|
||||||
unsigned int volume = get_volume();
|
unsigned int volume = get_volume();
|
||||||
for (unsigned int i = 0; i < volume; ++i) {
|
for (unsigned int i = 0; i < volume; ++i) {
|
||||||
|
@ -153,43 +153,14 @@ void VoxelMap::get_buffer_copy(Vector3i min_pos, VoxelBuffer &dst_buffer, unsign
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoxelMap::remove_blocks_not_in_area(Vector3i min, Vector3i max) {
|
|
||||||
|
|
||||||
Vector3i::sort_min_max(min, max);
|
|
||||||
|
|
||||||
Vector<Vector3i> to_remove;
|
|
||||||
const Vector3i *key = NULL;
|
|
||||||
|
|
||||||
while (key = _blocks.next(key)) {
|
|
||||||
|
|
||||||
VoxelBlock *block_ref = _blocks.get(*key);
|
|
||||||
ERR_FAIL_COND(block_ref == NULL); // Should never trigger
|
|
||||||
|
|
||||||
if (block_ref->pos.is_contained_in(min, max)) {
|
|
||||||
|
|
||||||
//if (_observer)
|
|
||||||
// _observer->block_removed(block);
|
|
||||||
|
|
||||||
to_remove.push_back(*key);
|
|
||||||
|
|
||||||
if (block_ref == _last_accessed_block)
|
|
||||||
_last_accessed_block = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (unsigned int i = 0; i < to_remove.size(); ++i) {
|
|
||||||
_blocks.erase(to_remove[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void VoxelMap::clear() {
|
void VoxelMap::clear() {
|
||||||
const Vector3i *key = NULL;
|
const Vector3i *key = NULL;
|
||||||
while (key = _blocks.next(key)) {
|
while (key = _blocks.next(key)) {
|
||||||
VoxelBlock *block_ref = _blocks.get(*key);
|
VoxelBlock *block_ptr = _blocks.get(*key);
|
||||||
if (block_ref == NULL) {
|
if (block_ptr == NULL) {
|
||||||
OS::get_singleton()->printerr("Unexpected NULL in VoxelMap::clear()");
|
OS::get_singleton()->printerr("Unexpected NULL in VoxelMap::clear()");
|
||||||
}
|
}
|
||||||
memdelete(block_ref);
|
memdelete(block_ptr);
|
||||||
}
|
}
|
||||||
_blocks.clear();
|
_blocks.clear();
|
||||||
_last_accessed_block = NULL;
|
_last_accessed_block = NULL;
|
||||||
|
48
voxel_map.h
48
voxel_map.h
@ -50,7 +50,53 @@ public:
|
|||||||
// Moves the given buffer into a block of the map. The buffer is referenced, no copy is made.
|
// Moves the given buffer into a block of the map. The buffer is referenced, no copy is made.
|
||||||
void set_block_buffer(Vector3i bpos, Ref<VoxelBuffer> buffer);
|
void set_block_buffer(Vector3i bpos, Ref<VoxelBuffer> buffer);
|
||||||
|
|
||||||
void remove_blocks_not_in_area(Vector3i min, Vector3i max);
|
struct NoAction {
|
||||||
|
inline void operator()(VoxelBlock *block) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Action_T>
|
||||||
|
void remove_block(Vector3i bpos, Action_T pre_delete) {
|
||||||
|
if(_last_accessed_block && _last_accessed_block->pos == bpos)
|
||||||
|
_last_accessed_block = NULL;
|
||||||
|
VoxelBlock **pptr = _blocks.getptr(bpos);
|
||||||
|
if(pptr) {
|
||||||
|
VoxelBlock *block = *pptr;
|
||||||
|
ERR_FAIL_COND(block == NULL);
|
||||||
|
pre_delete(block);
|
||||||
|
memdelete(block);
|
||||||
|
_blocks.erase(bpos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*template <typename Action_T>
|
||||||
|
void remove_blocks_not_in_area(Vector3i min, Vector3i max, Action_T pre_delete = NoAction()) {
|
||||||
|
|
||||||
|
Vector3i::sort_min_max(min, max);
|
||||||
|
|
||||||
|
Vector<Vector3i> to_remove;
|
||||||
|
const Vector3i *key = NULL;
|
||||||
|
|
||||||
|
while (key = _blocks.next(key)) {
|
||||||
|
|
||||||
|
VoxelBlock *block_ptr = _blocks.get(*key);
|
||||||
|
ERR_FAIL_COND(block_ptr == NULL); // Should never trigger
|
||||||
|
|
||||||
|
if (block_ptr->pos.is_contained_in(min, max)) {
|
||||||
|
|
||||||
|
to_remove.push_back(*key);
|
||||||
|
|
||||||
|
if (block_ptr == _last_accessed_block)
|
||||||
|
_last_accessed_block = NULL;
|
||||||
|
|
||||||
|
pre_delete(block_ptr);
|
||||||
|
memdelete(block_ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < to_remove.size(); ++i) {
|
||||||
|
_blocks.erase(to_remove[i]);
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
VoxelBlock *get_block(Vector3i bpos);
|
VoxelBlock *get_block(Vector3i bpos);
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ void VoxelMesherSmooth::build_mesh(const VoxelBuffer &voxels, unsigned int chann
|
|||||||
// Each 2x2 voxel group is a "cell"
|
// Each 2x2 voxel group is a "cell"
|
||||||
|
|
||||||
if(voxels.is_uniform(channel)) {
|
if(voxels.is_uniform(channel)) {
|
||||||
// Nothing to extract, because constant isolevels never cross the surface
|
// Nothing to extract, because constant isolevels never cross the threshold and describe no surface
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#include "voxel_terrain.h"
|
#include "voxel_terrain.h"
|
||||||
#include "voxel_raycast.h"
|
#include "voxel_raycast.h"
|
||||||
|
#include "rect3i.h"
|
||||||
#include <os/os.h>
|
#include <os/os.h>
|
||||||
#include <scene/3d/mesh_instance.h>
|
#include <scene/3d/mesh_instance.h>
|
||||||
#include <engine.h>
|
#include <engine.h>
|
||||||
@ -13,9 +14,11 @@ VoxelTerrain::VoxelTerrain()
|
|||||||
_mesher_smooth = Ref<VoxelMesherSmooth>(memnew(VoxelMesherSmooth));
|
_mesher_smooth = Ref<VoxelMesherSmooth>(memnew(VoxelMesherSmooth));
|
||||||
|
|
||||||
_view_distance_blocks = 8;
|
_view_distance_blocks = 8;
|
||||||
|
_last_view_distance_blocks = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector3i g_viewer_block_pos; // TODO UGLY! Lambdas or pointers needed...
|
// TODO UGLY! Lambdas or pointers needed... DO NOT use this outside of lambdas!
|
||||||
|
Vector3i g_viewer_block_pos;
|
||||||
|
|
||||||
// Sorts distance to viewer
|
// Sorts distance to viewer
|
||||||
struct BlockUpdateComparator {
|
struct BlockUpdateComparator {
|
||||||
@ -62,7 +65,8 @@ void VoxelTerrain::_get_property_list(List<PropertyInfo> *p_list) const {
|
|||||||
void VoxelTerrain::set_provider(Ref<VoxelProvider> provider) {
|
void VoxelTerrain::set_provider(Ref<VoxelProvider> provider) {
|
||||||
if(provider != _provider) {
|
if(provider != _provider) {
|
||||||
_provider = provider;
|
_provider = provider;
|
||||||
make_all_view_dirty();
|
// The whole map might change, so make all area dirty
|
||||||
|
make_all_view_dirty_deferred();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +87,9 @@ void VoxelTerrain::set_voxel_library(Ref<VoxelLibrary> library) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
_mesher->set_library(library);
|
_mesher->set_library(library);
|
||||||
make_all_view_dirty();
|
|
||||||
|
// Voxel appearance might completely change
|
||||||
|
make_all_view_dirty_deferred();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,16 +105,13 @@ void VoxelTerrain::set_view_distance(int distance_in_voxels) {
|
|||||||
ERR_FAIL_COND(distance_in_voxels < 0)
|
ERR_FAIL_COND(distance_in_voxels < 0)
|
||||||
int d = distance_in_voxels / _map->get_block_size();
|
int d = distance_in_voxels / _map->get_block_size();
|
||||||
if(d != _view_distance_blocks) {
|
if(d != _view_distance_blocks) {
|
||||||
|
print_line(String("View distance changed from ") + String::num(_view_distance_blocks) + String(" blocks to ") + String::num(d));
|
||||||
_view_distance_blocks = d;
|
_view_distance_blocks = d;
|
||||||
make_all_view_dirty();
|
// Blocks too far away will be removed in _process, same for blocks to load
|
||||||
// TODO Immerge blocks too far away
|
|
||||||
// TODO Cancel updates that are scheduled too far away
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoxelTerrain::set_viewer_path(NodePath path) {
|
void VoxelTerrain::set_viewer_path(NodePath path) {
|
||||||
if (!path.is_empty())
|
|
||||||
ERR_FAIL_COND(get_viewer(path) == NULL);
|
|
||||||
_viewer_path = path;
|
_viewer_path = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +151,18 @@ void VoxelTerrain::make_block_dirty(Vector3i bpos) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VoxelTerrain::immerge_block(Vector3i bpos) {
|
||||||
|
|
||||||
|
ERR_FAIL_COND(_map.is_null());
|
||||||
|
|
||||||
|
// TODO Schedule block saving when supported
|
||||||
|
_map->remove_block(bpos, VoxelMap::NoAction());
|
||||||
|
|
||||||
|
_dirty_blocks.erase(bpos);
|
||||||
|
// Blocks in the update queue will be cancelled in _process,
|
||||||
|
// because it's too expensive to linear-search all blocks for each block
|
||||||
|
}
|
||||||
|
|
||||||
bool VoxelTerrain::is_block_dirty(Vector3i bpos) {
|
bool VoxelTerrain::is_block_dirty(Vector3i bpos) {
|
||||||
return _dirty_blocks.has(bpos);
|
return _dirty_blocks.has(bpos);
|
||||||
}
|
}
|
||||||
@ -164,10 +179,14 @@ void VoxelTerrain::make_blocks_dirty(Vector3i min, Vector3i size) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VoxelTerrain::make_all_view_dirty() {
|
void VoxelTerrain::make_all_view_dirty_deferred() {
|
||||||
Vector3i radius(_view_distance_blocks, _view_distance_blocks, _view_distance_blocks);
|
// This trick will regenerate all chunks in view, according to the view distance found during block updates.
|
||||||
// TODO Take viewer and fixed range into account
|
// The point of doing this instead of immediately scheduling updates is that it will
|
||||||
make_blocks_dirty(-radius, 2*radius);
|
// always use an up-to-date view distance, which is not necessarily loaded yet on initialization.
|
||||||
|
_last_view_distance_blocks = 0;
|
||||||
|
|
||||||
|
// Vector3i radius(_view_distance_blocks, _view_distance_blocks, _view_distance_blocks);
|
||||||
|
// make_blocks_dirty(-radius, 2*radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int get_border_index(int x, int max) {
|
inline int get_border_index(int x, int max) {
|
||||||
@ -302,12 +321,13 @@ void VoxelTerrain::_notification(int p_what) {
|
|||||||
case NOTIFICATION_EXIT_TREE:
|
case NOTIFICATION_EXIT_TREE:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NOTIFICATION_READY:
|
// case NOTIFICATION_READY:
|
||||||
// TODO This should also react to viewer movement
|
// break;
|
||||||
make_all_view_dirty();
|
|
||||||
break;
|
|
||||||
|
|
||||||
// TODO Listen for transform changes
|
// TODO Listen for transform changes
|
||||||
|
// TODO Listen for NOTIFICATION_VISIBILITY_CHANGED
|
||||||
|
// TODO Listen for NOTIFICATION_ENTER_WORLD
|
||||||
|
// TODO Listen for NOTIFICATION_EXIT_WORLD
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -319,19 +339,79 @@ void VoxelTerrain::_process() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void VoxelTerrain::update_blocks() {
|
void VoxelTerrain::update_blocks() {
|
||||||
|
|
||||||
OS &os = *OS::get_singleton();
|
OS &os = *OS::get_singleton();
|
||||||
|
Engine &engine = *Engine::get_singleton();
|
||||||
|
|
||||||
ERR_FAIL_COND(_map.is_null());
|
ERR_FAIL_COND(_map.is_null());
|
||||||
|
|
||||||
// Get viewer location
|
// Get viewer location
|
||||||
|
// TODO Transform to local (Spatial Transform)
|
||||||
|
Vector3i viewer_block_pos;
|
||||||
|
if(engine.is_editor_hint()) {
|
||||||
|
// TODO Use editor's camera here
|
||||||
|
viewer_block_pos = Vector3i();
|
||||||
|
} else {
|
||||||
Spatial *viewer = get_viewer(_viewer_path);
|
Spatial *viewer = get_viewer(_viewer_path);
|
||||||
if (viewer)
|
if (viewer)
|
||||||
g_viewer_block_pos = _map->voxel_to_block(viewer->get_translation());
|
viewer_block_pos = _map->voxel_to_block(viewer->get_translation());
|
||||||
else
|
else
|
||||||
g_viewer_block_pos = Vector3i();
|
viewer_block_pos = Vector3i();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Find out which blocks need to appear and which need to be unloaded
|
||||||
|
|
||||||
|
//Vector3i viewer_block_pos_delta = _last_viewer_block_pos - viewer_block_pos;
|
||||||
|
Rect3i new_box = Rect3i::from_center_extents(viewer_block_pos, Vector3i(_view_distance_blocks));
|
||||||
|
Rect3i prev_box = Rect3i::from_center_extents(_last_viewer_block_pos, Vector3i(_last_view_distance_blocks));
|
||||||
|
|
||||||
|
if(prev_box != new_box) {
|
||||||
|
//print_line(String("Loaded area changed: from ") + prev_box.to_string() + String(" to ") + new_box.to_string());
|
||||||
|
|
||||||
|
Rect3i bounds = Rect3i::get_bounding_box(prev_box, new_box);
|
||||||
|
Vector3i max = bounds.pos + bounds.size;
|
||||||
|
|
||||||
|
// TODO There should be a way to only iterate relevant blocks
|
||||||
|
Vector3i pos;
|
||||||
|
for(pos.z = bounds.pos.z; pos.z < max.z; ++pos.z) {
|
||||||
|
for(pos.y = bounds.pos.y; pos.y < max.y; ++pos.y) {
|
||||||
|
for(pos.x = bounds.pos.x; pos.x < max.x; ++pos.x) {
|
||||||
|
|
||||||
|
bool prev_contains = prev_box.contains(pos);
|
||||||
|
bool new_contains = new_box.contains(pos);
|
||||||
|
|
||||||
|
if(prev_contains && !new_contains) {
|
||||||
|
// Unload block
|
||||||
|
immerge_block(pos);
|
||||||
|
|
||||||
|
} else if(!prev_contains && new_contains) {
|
||||||
|
// Load or update block
|
||||||
|
make_block_dirty(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminate blocks in queue that aren't needed
|
||||||
|
for(int i = 0; i < _block_update_queue.size(); ++i) {
|
||||||
|
const Vector3i bpos = _block_update_queue[i];
|
||||||
|
if(!new_box.contains(bpos)) {
|
||||||
|
int last = _block_update_queue.size() - 1;
|
||||||
|
_block_update_queue[i] = _block_update_queue[last];
|
||||||
|
_block_update_queue.resize(last);
|
||||||
|
--i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_last_view_distance_blocks = _view_distance_blocks;
|
||||||
|
_last_viewer_block_pos = viewer_block_pos;
|
||||||
|
|
||||||
// Sort updates so nearest blocks are done first
|
// Sort updates so nearest blocks are done first
|
||||||
VOXEL_PROFILE_BEGIN("block_update_sorting")
|
VOXEL_PROFILE_BEGIN("block_update_sorting")
|
||||||
|
g_viewer_block_pos = viewer_block_pos;
|
||||||
_block_update_queue.sort_custom<BlockUpdateComparator>();
|
_block_update_queue.sort_custom<BlockUpdateComparator>();
|
||||||
VOXEL_PROFILE_END("block_update_sorting")
|
VOXEL_PROFILE_END("block_update_sorting")
|
||||||
|
|
||||||
@ -409,6 +489,8 @@ void VoxelTerrain::update_blocks() {
|
|||||||
_block_update_queue.resize(_block_update_queue.size() - 1);
|
_block_update_queue.resize(_block_update_queue.size() - 1);
|
||||||
_dirty_blocks.erase(block_pos);
|
_dirty_blocks.erase(block_pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//print_line(String("d:") + String::num(_dirty_blocks.size()) + String(", q:") + String::num(_block_update_queue.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline bool is_mesh_empty(Ref<Mesh> mesh_ref) {
|
static inline bool is_mesh_empty(Ref<Mesh> mesh_ref) {
|
||||||
|
@ -57,10 +57,12 @@ private:
|
|||||||
void update_blocks();
|
void update_blocks();
|
||||||
void update_block_mesh(Vector3i block_pos);
|
void update_block_mesh(Vector3i block_pos);
|
||||||
|
|
||||||
void make_all_view_dirty();
|
void make_all_view_dirty_deferred();
|
||||||
|
|
||||||
Spatial *get_viewer(NodePath path) const;
|
Spatial *get_viewer(NodePath path) const;
|
||||||
|
|
||||||
|
void immerge_block(Vector3i bpos);
|
||||||
|
|
||||||
// Observer events
|
// Observer events
|
||||||
//void block_removed(VoxelBlock & block);
|
//void block_removed(VoxelBlock & block);
|
||||||
|
|
||||||
@ -99,6 +101,8 @@ private:
|
|||||||
Ref<VoxelProvider> _provider;
|
Ref<VoxelProvider> _provider;
|
||||||
|
|
||||||
NodePath _viewer_path;
|
NodePath _viewer_path;
|
||||||
|
Vector3i _last_viewer_block_pos;
|
||||||
|
int _last_view_distance_blocks;
|
||||||
|
|
||||||
bool _generate_collisions;
|
bool _generate_collisions;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user