#include "mapblock_mesh.h"
#include "CMeshBuffer.h"
#include "client.h"
#include "mapblock.h"
#include "node_visuals.h"
#include "porting.h"
#include "shader.h"
#include "mesh.h"
#include "minimap.h"
#include "content_mapblock.h"
#include "util/tracy_wrapper.h"
#include "client/meshgen/collector.h"
#include "client/renderingengine.h"
#include <array>
#include <algorithm>
#include <cmath>
#include <cassert>
#include "client/texturesource.h"
#include <SMesh.h>
#include <IMeshBuffer.h>
#include <CMeshBuffer.h>
MeshMakeData
*/
MeshMakeData::MeshMakeData(const NodeDefManager *ndef,
u16 side_length, MeshGrid mesh_grid) :
m_side_length(side_length),
m_mesh_grid(mesh_grid),
m_nodedef(ndef)
{
assert(m_side_length > 0);
}
void MeshMakeData::fillBlockDataBegin(const v3s16 &blockpos)
{
m_blockpos = blockpos;
v3s16 blockpos_nodes = m_blockpos*MAP_BLOCKSIZE;
m_vmanip.clear();
VoxelArea voxel_area(blockpos_nodes - v3s16(1,1,1) * MAP_BLOCKSIZE,
blockpos_nodes + v3s16(1,1,1) * (m_side_length + MAP_BLOCKSIZE) - v3s16(1,1,1));
m_vmanip.addArea(voxel_area);
}
void MeshMakeData::fillSingleNode(MapNode data, MapNode padding)
{
m_blockpos = {0, 0, 0};
m_vmanip.clear();
const s16 sz = 3;
m_vmanip.addArea({v3s16(-sz), v3s16(sz)});
u32 count = m_vmanip.m_area.getVolume();
for (u32 i = 0; i < count; i++) {
m_vmanip.m_data[i] = padding;
m_vmanip.m_flags[i] &= ~VOXELFLAG_NO_DATA;
}
m_vmanip.setNodeNoEmerge({0, 0, 0}, data);
}
void MeshMakeData::setCrack(int crack_level, v3s16 crack_pos)
{
if (crack_level >= 0)
m_crack_pos_relative = crack_pos - m_blockpos*MAP_BLOCKSIZE;
}
Light and vertex color functions
*/
Calculate non-smooth lighting at interior of node.
Single light bank.
*/
static u8 getInteriorLight(enum LightBank bank, MapNode n, s32 increment,
const NodeDefManager *ndef)
{
u8 light = n.getLight(bank, ndef->getLightingFlags(n));
light = rangelim(light + increment, 0, LIGHT_SUN);
return decode_light(light);
}
Calculate non-smooth lighting at interior of node.
Both light banks.
*/
u16 getInteriorLight(MapNode n, s32 increment, const NodeDefManager *ndef)
{
u16 day = getInteriorLight(LIGHTBANK_DAY, n, increment, ndef);
u16 night = getInteriorLight(LIGHTBANK_NIGHT, n, increment, ndef);
return day | (night << 8);
}
void get_sunlight_color(video::SColorf *sunlight, u32 daynight_ratio)
{
f32 rg = daynight_ratio / 1000.0f - 0.04f;
f32 b = (0.98f * daynight_ratio) / 1000.0f + 0.078f;
sunlight->r = rg;
sunlight->g = rg;
sunlight->b = b;
}
void final_color_blend(video::SColor *result,
u16 light, u32 daynight_ratio)
{
video::SColorf dayLight;
get_sunlight_color(&dayLight, daynight_ratio);
final_color_blend(result,
encode_light(light, 0), dayLight);
}
void final_color_blend(video::SColor *result,
const video::SColor &data, const video::SColorf &dayLight)
{
static const video::SColorf artificialColor(1.04f, 1.04f, 1.04f);
video::SColorf c(data);
f32 n = 1 - c.a;
f32 r = c.r * (c.a * dayLight.r + n * artificialColor.r) * 2.0f;
f32 g = c.g * (c.a * dayLight.g + n * artificialColor.g) * 2.0f;
f32 b = c.b * (c.a * dayLight.b + n * artificialColor.b) * 2.0f;
static const u8 emphase_blue_when_dark[32] = {
1, 4, 6, 6, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
b += emphase_blue_when_dark[core::clamp((s32) ((r + g + b) / 3 * 255),
0, 255) / 8] / 255.0f;
result->setRed(core::clamp((s32) (r * 255.0f), 0, 255));
result->setGreen(core::clamp((s32) (g * 255.0f), 0, 255));
result->setBlue(core::clamp((s32) (b * 255.0f), 0, 255));
}
Mesh generation helpers
*/
Gets nth node tile (0 <= n <= 5).
*/
void getNodeTileN(MapNode mn, const v3s16 &p, u8 tileindex, MeshMakeData *data, TileSpec &tile)
{
const NodeDefManager *ndef = data->m_nodedef;
const ContentFeatures &f = ndef->get(mn);
tile = f.visuals->tiles[tileindex];
bool has_crack = p == data->m_crack_pos_relative;
for (TileLayer &layer : tile.layers) {
if (layer.empty())
continue;
if (!layer.has_color)
layer.color = f.visuals->getColor(f, mn.param2);
if (has_crack)
layer.material_flags |= MATERIAL_FLAG_CRACK;
}
}
Gets node tile given a face direction.
*/
void getNodeTile(MapNode mn, const v3s16 &p, const v3s16 &dir, MeshMakeData *data, TileSpec &tile)
{
const NodeDefManager *ndef = data->m_nodedef;
assert(dir.X * dir.X + dir.Y * dir.Y + dir.Z * dir.Z <= 1);
u8 dir_i = (dir.X + 2 * dir.Y + 3 * dir.Z) & 7;
u8 facedir = mn.getFaceDir(ndef, true);
static constexpr auto
R0 = TileRotation::None,
R1 = TileRotation::R90,
R2 = TileRotation::R180,
R3 = TileRotation::R270;
static const struct {
u8 tile;
TileRotation rotation;
} dir_to_tile[24][8] = {
{{0,R0}, {2,R0}, {0,R0}, {4,R0}, {0,R0}, {5,R0}, {1,R0}, {3,R0}},
{{0,R0}, {4,R0}, {0,R3}, {3,R0}, {0,R0}, {2,R0}, {1,R1}, {5,R0}},
{{0,R0}, {3,R0}, {0,R2}, {5,R0}, {0,R0}, {4,R0}, {1,R2}, {2,R0}},
{{0,R0}, {5,R0}, {0,R1}, {2,R0}, {0,R0}, {3,R0}, {1,R3}, {4,R0}},
{{0,R0}, {2,R3}, {5,R0}, {0,R2}, {0,R0}, {1,R0}, {4,R2}, {3,R1}},
{{0,R0}, {4,R3}, {2,R0}, {0,R1}, {0,R0}, {1,R1}, {3,R2}, {5,R1}},
{{0,R0}, {3,R3}, {4,R0}, {0,R0}, {0,R0}, {1,R2}, {5,R2}, {2,R1}},
{{0,R0}, {5,R3}, {3,R0}, {0,R3}, {0,R0}, {1,R3}, {2,R2}, {4,R1}},
{{0,R0}, {2,R1}, {4,R2}, {1,R2}, {0,R0}, {0,R0}, {5,R0}, {3,R3}},
{{0,R0}, {4,R1}, {3,R2}, {1,R3}, {0,R0}, {0,R3}, {2,R0}, {5,R3}},
{{0,R0}, {3,R1}, {5,R2}, {1,R0}, {0,R0}, {0,R2}, {4,R0}, {2,R3}},
{{0,R0}, {5,R1}, {2,R2}, {1,R1}, {0,R0}, {0,R1}, {3,R0}, {4,R3}},
{{0,R0}, {0,R3}, {3,R3}, {4,R1}, {0,R0}, {5,R3}, {2,R3}, {1,R3}},
{{0,R0}, {0,R2}, {5,R3}, {3,R1}, {0,R0}, {2,R3}, {4,R3}, {1,R0}},
{{0,R0}, {0,R1}, {2,R3}, {5,R1}, {0,R0}, {4,R3}, {3,R3}, {1,R1}},
{{0,R0}, {0,R0}, {4,R3}, {2,R1}, {0,R0}, {3,R3}, {5,R3}, {1,R2}},
{{0,R0}, {1,R1}, {2,R1}, {4,R3}, {0,R0}, {5,R1}, {3,R1}, {0,R1}},
{{0,R0}, {1,R2}, {4,R1}, {3,R3}, {0,R0}, {2,R1}, {5,R1}, {0,R0}},
{{0,R0}, {1,R3}, {3,R1}, {5,R3}, {0,R0}, {4,R1}, {2,R1}, {0,R3}},
{{0,R0}, {1,R0}, {5,R1}, {2,R3}, {0,R0}, {3,R1}, {4,R1}, {0,R2}},
{{0,R0}, {3,R2}, {1,R2}, {4,R2}, {0,R0}, {5,R2}, {0,R2}, {2,R2}},
{{0,R0}, {5,R2}, {1,R3}, {3,R2}, {0,R0}, {2,R2}, {0,R1}, {4,R2}},
{{0,R0}, {2,R2}, {1,R0}, {5,R2}, {0,R0}, {4,R2}, {0,R0}, {3,R2}},
{{0,R0}, {4,R2}, {1,R1}, {2,R2}, {0,R0}, {3,R2}, {0,R3}, {5,R2}}
};
getNodeTileN(mn, p, dir_to_tile[facedir][dir_i].tile, data, tile);
tile.rotation = tile.world_aligned ? TileRotation::None : dir_to_tile[facedir][dir_i].rotation;
}
MapBlockBspTree
*/
void MapBlockBspTree::buildTree(const std::vector<MeshTriangle> *triangles, u16 side_length)
{
this->triangles = triangles;
nodes.clear();
assert(triangles->size() <= 0x7FFFFFFFL);
std::vector<s32> indexes;
indexes.reserve(triangles->size());
for (u32 i = 0; i < triangles->size(); i++)
indexes.push_back(i);
if (!indexes.empty()) {
root = buildTree(v3f(1, 0, 0), v3f((side_length + 1) * 0.5f * BS), side_length * 0.25f * BS, indexes, 0);
} else {
root = -1;
}
}
* @brief Find a candidate plane to split a set of triangles in two
*
* The candidate plane is represented by one of the triangles from the set.
*
* @param list Vector of indexes of the triangles in the set
* @param triangles Vector of all triangles in the BSP tree
* @return Address of the triangle that represents the proposed split plane
*/
static const MeshTriangle *findSplitCandidate(const std::vector<s32> &list, const std::vector<MeshTriangle> &triangles)
{
v3f center(0, 0, 0);
size_t n = list.size();
for (s32 i : list) {
center += triangles[i].centroid / n;
}
const MeshTriangle *candidate_triangle = &triangles[list[0]];
const MeshTriangle *ith_triangle;
for (s32 i : list) {
ith_triangle = &triangles[i];
if (ith_triangle->areaSQ > candidate_triangle->areaSQ ||
(ith_triangle->areaSQ == candidate_triangle->areaSQ &&
ith_triangle->centroid.getDistanceFromSQ(center) < candidate_triangle->centroid.getDistanceFromSQ(center))) {
candidate_triangle = ith_triangle;
}
}
return candidate_triangle;
}
s32 MapBlockBspTree::buildTree(v3f normal, v3f origin, float delta, const std::vector<s32> &list, u32 depth)
{
if (list.empty())
return -1;
if (list.size() == 1 || delta < 0.01) {
nodes.emplace_back(normal, origin, list, -1, -1);
return nodes.size() - 1;
}
std::vector<s32> front_list;
std::vector<s32> back_list;
std::vector<s32> node_list;
for (s32 i : list) {
const MeshTriangle &triangle = (*triangles)[i];
float factor = normal.dotProduct(triangle.centroid - origin);
if (factor == 0)
node_list.push_back(i);
else if (factor > 0)
front_list.push_back(i);
else
back_list.push_back(i);
}
v3f candidate_normal(normal.Z, normal.X, normal.Y);
float candidate_delta = delta;
if (depth % 3 == 2)
candidate_delta /= 2;
s32 front_index = -1;
s32 back_index = -1;
if (!front_list.empty()) {
v3f next_normal = candidate_normal;
v3f next_origin = origin + delta * normal;
float next_delta = candidate_delta;
if (next_delta < 5) {
const MeshTriangle *candidate = findSplitCandidate(front_list, *triangles);
next_normal = candidate->getNormal();
next_origin = candidate->centroid;
}
front_index = buildTree(next_normal, next_origin, next_delta, front_list, depth + 1);
if (back_list.empty() && node_list.empty())
return front_index;
}
if (!back_list.empty()) {
v3f next_normal = candidate_normal;
v3f next_origin = origin - delta * normal;
float next_delta = candidate_delta;
if (next_delta < 5) {
const MeshTriangle *candidate = findSplitCandidate(back_list, *triangles);
next_normal = candidate->getNormal();
next_origin = candidate->centroid;
}
back_index = buildTree(next_normal, next_origin, next_delta, back_list, depth + 1);
if (front_list.empty() && node_list.empty())
return back_index;
}
nodes.emplace_back(normal, origin, node_list, front_index, back_index);
return nodes.size() - 1;
}
void MapBlockBspTree::traverse(s32 node, v3f viewpoint, std::vector<s32> &output) const
{
if (node < 0) return;
const TreeNode &n = nodes[node];
float factor = n.normal.dotProduct(viewpoint - n.origin);
if (factor > 0)
traverse(n.back_ref, viewpoint, output);
else
traverse(n.front_ref, viewpoint, output);
if (factor != 0)
for (s32 i : n.triangle_refs)
output.push_back(i);
if (factor > 0)
traverse(n.front_ref, viewpoint, output);
else
traverse(n.back_ref, viewpoint, output);
}
PartialMeshBuffer
*/
void PartialMeshBuffer::draw(video::IVideoDriver *driver) const
{
const auto pType = m_buffer->getPrimitiveType();
driver->drawBuffers(m_buffer->getVertexBuffer(), m_indices.get(),
m_indices->getPrimitiveCount(pType), pType);
}
MapBlockMesh
*/
static void applyColorAndMerge(std::vector<PreMeshBuffer> &prebuffers)
{
for (auto &p : prebuffers) {
p.applyTileColor();
p.layer.has_color = false;
p.layer.color = 0;
}
std::unordered_map<TileLayer, size_t> seen;
for (size_t i = 0; i < prebuffers.size(); i++) {
PreMeshBuffer &p = prebuffers[i];
auto it = seen.find(p.layer);
if (it == seen.end()) {
seen[p.layer] = i;
continue;
}
auto &dst = prebuffers[it->second];
assert(p.layer == dst.layer);
if (dst.append(p)) {
p = PreMeshBuffer();
} else {
it->second = i;
}
}
prebuffers.erase(std::remove_if(prebuffers.begin(), prebuffers.end(),
[] (const PreMeshBuffer &p) {
return p.empty();
}), prebuffers.end());
}
MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data):
m_tsrc(client->getTextureSource()),
m_shdrsrc(client->getShaderSource()),
m_bounding_sphere_center((data->m_side_length * 0.5f - 0.5f) * BS),
m_animation_force_timer(0),
m_last_crack(-1)
{
ZoneScoped;
for (auto &m : m_mesh)
m = make_irr<scene::SMesh>();
auto mesh_grid = data->m_mesh_grid;
v3s16 bp = data->m_blockpos;
if (mesh_grid.isMeshPos(bp) && data->m_generate_minimap) {
m_minimap_mapblocks.resize(mesh_grid.getCellVolume(), nullptr);
v3s16 ofs;
for (ofs.Z = 0; ofs.Z < mesh_grid.cell_size; ofs.Z++)
for (ofs.Y = 0; ofs.Y < mesh_grid.cell_size; ofs.Y++)
for (ofs.X = 0; ofs.X < mesh_grid.cell_size; ofs.X++) {
v3s16 p = (bp + ofs) * MAP_BLOCKSIZE;
if (data->m_vmanip.getNodeNoExNoEmerge(p).getContent() != CONTENT_IGNORE) {
MinimapMapblock *block = new MinimapMapblock;
m_minimap_mapblocks[mesh_grid.getOffsetIndex(ofs)] = block;
block->getMinimapNodes(&data->m_vmanip, data->m_nodedef, p);
}
}
}
v3f offset = intToFloat((data->m_blockpos - mesh_grid.getMeshPos(data->m_blockpos)) * MAP_BLOCKSIZE, BS);
MeshCollector collector(m_bounding_sphere_center, offset);
{
MapblockMeshGenerator(data, &collector).generate();
}
Convert MeshCollector to SMesh
*/
m_bounding_radius = std::sqrt(collector.m_bounding_radius_sq);
for (int layer = 0; layer < MAX_TILE_LAYERS; layer++) {
scene::SMesh *mesh = static_cast<scene::SMesh *>(m_mesh[layer].get());
applyColorAndMerge(collector.prebuffers[layer]);
for (size_t i = 0; i < collector.prebuffers[layer].size(); i++) {
PreMeshBuffer &p = collector.prebuffers[layer][i];
assert(!p.empty());
if (p.layer.material_flags & MATERIAL_FLAG_ANIMATION) {
m_animation_info.emplace(std::make_pair(layer, i), AnimationInfo(p.layer));
}
video::SMaterial material;
material.FogEnable = true;
material.forEachTexture([] (auto &tex) {
tex.MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST;
tex.MagFilter = video::ETMAGF_NEAREST;
});
{
material.MaterialType = m_shdrsrc->getShaderInfo(
p.layer.shader_id).material;
p.layer.applyMaterialOptions(material, layer);
}
if (p.layer.material_flags & MATERIAL_FLAG_CRACK) {
auto *t = m_tsrc->getTextureForMesh("crack_anylength.png");
material.setTexture(TEXTURE_LAYER_CRACK, t);
material.MaterialTypeParam =
packCrackMaterialParam(-1, MYMAX(1, p.layer.scale));
m_crack_materials.emplace_back(layer, i);
}
scene::SMeshBuffer *buf = new scene::SMeshBuffer();
buf->Material = material;
if (p.layer.isTransparent()) {
buf->append(&p.vertices[0], p.vertices.size(), nullptr, 0);
MeshTriangle t;
t.buffer = buf;
m_transparent_triangles.reserve(p.indices.size() / 3);
for (u32 i = 0; i < p.indices.size(); i += 3) {
t.p1 = p.indices[i];
t.p2 = p.indices[i + 1];
t.p3 = p.indices[i + 2];
t.updateAttributes();
m_transparent_triangles.push_back(t);
}
} else {
buf->append(&p.vertices[0], p.vertices.size(),
&p.indices[0], p.indices.size());
}
mesh->addMeshBuffer(buf);
buf->drop();
}
if (mesh) {
mesh->setHardwareMappingHint(scene::EHM_STATIC);
}
}
m_bsp_tree.buildTree(&m_transparent_triangles, data->m_side_length);
m_has_animation =
!m_crack_materials.empty() ||
!m_animation_info.empty();
}
MapBlockMesh::~MapBlockMesh()
{
size_t sz = 0;
for (auto &&m : m_mesh) {
for (u32 i = 0; i < m->getMeshBufferCount(); i++)
sz += m->getMeshBuffer(i)->getSize();
m.reset();
}
for (MinimapMapblock *block : m_minimap_mapblocks)
delete block;
porting::TrackFreedMemory(sz);
}
bool MapBlockMesh::animate(bool faraway, float time, int crack,
u32 daynight_ratio)
{
if (!m_has_animation) {
m_animation_force_timer = 100000;
return false;
}
m_animation_force_timer = myrand_range(5, 100);
if (crack != m_last_crack) {
for (auto &it : m_crack_materials) {
scene::IMeshBuffer *buf = m_mesh[it.first]->getMeshBuffer(it.second);
assert(buf);
video::SMaterial &mat = buf->getMaterial();
auto pair = unpackCrackMaterialParam(mat.MaterialTypeParam);
pair.first = crack;
mat.MaterialTypeParam = packCrackMaterialParam(pair.first, pair.second);
}
m_last_crack = crack;
}
for (auto &it : m_animation_info) {
scene::IMeshBuffer *buf = m_mesh[it.first.first]->getMeshBuffer(it.first.second);
assert(buf);
video::SMaterial &material = buf->getMaterial();
it.second.updateTexture(material, time);
}
return true;
}
void MapBlockMesh::updateTransparentBuffers(v3f camera_pos, v3s16 block_pos,
bool group_by_buffers)
{
if (m_transparent_triangles.empty())
return;
v3f block_posf = intToFloat(block_pos * MAP_BLOCKSIZE, BS);
v3f rel_camera_pos = camera_pos - block_posf;
std::vector<s32> triangle_refs;
m_bsp_tree.traverse(rel_camera_pos, triangle_refs);
m_transparent_buffers_consolidated = false;
m_transparent_buffers.clear();
std::vector<std::pair<scene::SMeshBuffer *, std::vector<u16>>> ordered_strains;
std::unordered_map<scene::SMeshBuffer *, size_t> strain_idxs;
if (group_by_buffers) {
scene::SMeshBuffer *current_buffer = nullptr;
for (auto it = triangle_refs.rbegin(); it != triangle_refs.rend(); ++it) {
const auto &t = m_transparent_triangles[*it];
if (current_buffer == t.buffer)
continue;
current_buffer = t.buffer;
auto [_it2, is_new] =
strain_idxs.emplace(current_buffer, ordered_strains.size());
if (is_new)
ordered_strains.emplace_back(current_buffer, std::vector<u16>{});
}
}
scene::SMeshBuffer *current_buffer = nullptr;
std::vector<u16> *current_strain = nullptr;
for (auto i : triangle_refs) {
const auto &t = m_transparent_triangles[i];
if (current_buffer != t.buffer) {
current_buffer = t.buffer;
if (group_by_buffers) {
auto it = strain_idxs.find(current_buffer);
sanity_check(it != strain_idxs.end());
current_strain = &ordered_strains[it->second].second;
} else {
ordered_strains.emplace_back(current_buffer, std::vector<u16>{});
current_strain = &ordered_strains.back().second;
}
}
current_strain->push_back(t.p1);
current_strain->push_back(t.p2);
current_strain->push_back(t.p3);
}
m_transparent_buffers.reserve(ordered_strains.size());
if (group_by_buffers) {
for (auto it = ordered_strains.rbegin(); it != ordered_strains.rend(); ++it)
m_transparent_buffers.emplace_back(it->first, std::move(it->second));
} else {
for (auto it = ordered_strains.begin(); it != ordered_strains.end(); ++it)
m_transparent_buffers.emplace_back(it->first, std::move(it->second));
}
}
void MapBlockMesh::consolidateTransparentBuffers()
{
if (m_transparent_buffers_consolidated)
return;
m_transparent_buffers.clear();
scene::SMeshBuffer *current_buffer = nullptr;
std::vector<u16> current_strain;
for (const auto &t : m_transparent_triangles) {
if (current_buffer != t.buffer) {
if (current_buffer != nullptr) {
this->m_transparent_buffers.emplace_back(current_buffer, std::move(current_strain));
current_strain.clear();
}
current_buffer = t.buffer;
}
current_strain.push_back(t.p1);
current_strain.push_back(t.p2);
current_strain.push_back(t.p3);
}
if (!current_strain.empty()) {
this->m_transparent_buffers.emplace_back(current_buffer, std::move(current_strain));
}
m_transparent_buffers_consolidated = true;
}
video::SColor encode_light(LightPair light, u8 emissive_light)
{
u32 day = light.lightDay;
u32 night = light.lightNight;
night += (emissive_light * 5) / 2;
if (night > 255)
night = 255;
u32 r;
u32 b;
if (day < night) {
r = 0;
b = night / 2;
} else {
if (day == 0)
return video::SColor(0);
r = 255 - 255 * night / day;
b = day / 2;
}
return video::SColor(r, b, b, b);
}
video::SColor encode_light(u16 light, u8 emissive_light)
{
return encode_light(LightPair(light), emissive_light);
}
u8 get_solid_sides(MeshMakeData *data)
{
v3s16 blockpos_nodes = data->m_blockpos * MAP_BLOCKSIZE;
const NodeDefManager *ndef = data->m_nodedef;
const u16 side = data->m_side_length;
assert(data->m_vmanip.m_area.contains(blockpos_nodes + v3s16(side - 1)));
u8 result = 0x3F;
for (s16 i = 0; i < side && result != 0; i++)
for (s16 j = 0; j < side && result != 0; j++) {
v3s16 positions[6] = {
v3s16(0, i, j),
v3s16(side - 1, i, j),
v3s16(i, 0, j),
v3s16(i, side - 1, j),
v3s16(i, j, 0),
v3s16(i, j, side - 1)
};
for (u8 k = 0; k < 6; k++) {
const MapNode &top = data->m_vmanip.getNodeRefUnsafe(blockpos_nodes + positions[k]);
if (NDT_solidness[ndef->get(top).drawtype] != 2)
result &= ~(1 << k);
}
}
return result;
}