Diseño e impresión 3D con Minetest

No dejarán nunca de sorprenderme. Y eso que al principio me costó convencerles de que trabajar en equipo era fundamental para conseguir el objetivo final: construir un castillo de manera colaborativa en un mundo virtual para luego imprimirlo en 3D! Juntar a una veintena de muchachos de Primaria en un mismo escenario con acceso ilimitado a fuentes de lava, torrentes de agua, fuego y dinamita y pretender que no lo utilicen unos contra otros, sin gritos, de forma «ordenada»… es una prueba digna de Hércules. Yo no la superé a la primera, ni a la segunda,…

Pero bastó enseñarles una diminuta maqueta impresa hecha con el Minetest para que cambiaran y se pusieran «en serio». Os adjunto un pequeño tutorial para replicar la «experiencia» y unas fotos con los resultados.

¡Artículo Actualizado!: http://diocesanos.es/blogs/equipotic/2019/02/12/nueva-version-del-modulo-para-imprimir-en-3d-modelos-de-minetest/

Minetest

Puedes descargarlo de forma gratuita desde la web original del programa:

http://www.minetest.net/downloads/

Hay versiones para Windows, Mac, Android, FreeBSD y Linux. El tutorial se centra en este último, y en concreto en el “sabor” Ubuntu disponible en los colegios.

Instalación e inicio primera vez

Abre un terminal y ejecuta las siguientes órdenes:

sudo apt-get update
sudo apt-get install minetest openscad slic3r

Ejecútalo, bien desde el menú “Aplicaciones” → “Juegos” o utilizando las teclas “ALT + F2” en caso de no estar disponible ese menú:

Ajusta las opciones para un equipo con bajos recursos. Ve a la solapa “Configuración” y desactiva todas las opciones tal como se muestra en la figura, en especial “Iluminación suave”, “Habilitar partículas”, “Nubes 3D” y “Sombreadores”. Las opciones de “Texturizado” también al mínimo.

Cierra el programa a continuación.

Creación del “mod“ de exportación

Minetest no viene preparado para generar el fichero de datos en el formato que necesitamos. Pero sí tiene un mecanismo bien documentado para la creación de complementos y modificaciones (mods) para dotarle de nuevas características. Para más información consulta los siguientes enlaces:

Opción 1: Como módulo local para el usuario del equipo

Creamos la siguiente estructura de archivos y carpetas (con el usuario “alumno”):

mkdir -p /home/alumno/.minetest/mods
mkdir -p /home/alumno/.minetest/mods/openscad
mkdir -p /home/alumno/.minetest/mods/openscad/textures
touch /home/alumno/.minetest/mods/openscad/depends.txt
touch /home/alumno/.minetest/mods/openscad/init.lua

mods/
└── openscad/
    ├── depends.txt
    ├── init.lua
    └── textures/

Opción 2: Como módulo para un servidor dedicado

Creamos la siguiente estructura de archivos y carpetas (con el usuario “root” o el dedicado al server):

mkdir -p /var/games/minetest-server/.minetest/mods
mkdir -p /var/games/minetest-server/.minetest/mods/openscad
mkdir -p /var/games/minetest-server/.minetest/mods/openscad/textures
touch /var/games/minetest-server/.minetest/mods/openscad/depends.txt
touch /var/games/minetest-server/.minetest/mods/openscad/init.lua

mods/
└── openscad/
    ├── depends.txt
    ├── init.lua
    └── textures/

Programa de exportación

Edita ahora el fichero “init.lua” y copia el siguiente código:

-- Módulo de exportación a OpenSCAD para impresión 3D
-- Copyright(C)2017 - David Martín Pascual
-- Versión v1.2

-- Proceso de exportación
exportanodos = {

   bloquesignorar = {
        ['air'] = 1,
        ['ignore'] = 1,
        ['unknown'] = 1
   },

   -- Lista de tipos de bloque que se tendrán en cuenta para el modelo a exportar
   bloquesadmitidos = {
        -- Bloques sólidos 1x1x1
        ['default:acacia_wood'] = 1,
        ['default:aspen_wood'] = 1,
        ['default:brick'] = 1,
        ['default:bronzeblock'] = 1,
        ['default:clay'] = 1,
        ['default:coalblock'] = 1,
        ['default:cobble'] = 1,
        ['default:copperblock'] = 1,
        ['default:desert_cobble'] = 1,
        ['default:desert_sand'] = 1,
        ['default:desert_stone'] = 1,
        ['default:desert_stone_block'] = 1,
        ['default:desert_stonebrick'] = 1,
        ['default:diamondblock'] = 1,
        ['default:dirt'] = 1,
        ['default:dirt_with_dry_grass'] = 1,
        ['default:dirt_with_grass'] = 1,
        ['default:dirt_with_snow'] = 1,
        ['default:goldblock'] = 1,
        ['default:gravel'] = 1,
        ['default:ice'] = 1,
        ['default:junglewood'] = 1,
        ['default:mese'] = 1,
        ['default:meselamp'] = 1,
        ['default:mossycobble'] = 1,
        ['default:obsidian_block'] = 1,
        ['default:obsidianbrick'] = 1,
        ['default:pine_wood'] = 1,
        ['default:sand'] = 1,
        ['default:sandstone'] = 1,
        ['default:sandstone_block'] = 1,
        ['default:sandstonebrick'] = 1,
        ['default:silver_sand'] = 1,
        ['default:snowblock'] = 1,
        ['default:steelblock'] = 1,
        ['default:stone'] = 1,
        ['default:stone_block'] = 1,
        ['default:stone_with_coal'] = 1,
        ['default:stone_with_copper'] = 1,
        ['default:stone_with_diamond'] = 1,
        ['default:stone_with_gold'] = 1,
        ['default:stone_with_iron'] = 1,
        ['default:stone_with_mese'] = 1,
        ['default:stonebrick'] = 1,
        ['default:wood'] = 1,
        ['wool:white'] = 1,
        ['wool:grey'] = 1,
        ['wool:dark_grey'] = 1,
        ['wool:black'] = 1,
        ['wool:blue'] = 1,
        ['wool:cyan'] = 1,
        ['wool:green'] = 1,
        ['wool:dark_green'] = 1,
        ['wool:yellow'] = 1,
        ['wool:orange'] = 1,
        ['wool:brown'] = 1,
        ['wool:red'] = 1,
        ['wool:pink'] = 1,
        ['wool:magenta'] = 1,
        ['wool:violet'] = 1,
         -- Muros delgados
        ['walls:cobble'] = 2,
        ['walls:mossycobble'] = 2,
        ['walls:desertcobble'] = 2,
        -- Puertas (dos alturas)
        ['doors:door_steel_a'] = 3,
        ['doors:door_steel_b'] = 3,
        ['doors:door_steel_b_1'] = 3,
        ['doors:door_steel_b_2'] = 3,
        ['doors:door_wood_a'] = 3,
        ['doors:door_wood_b'] = 3,
        ['doors:door_wood_b_1'] = 3,
        ['doors:door_wood_b_2'] = 3,
        -- Cerramientos
        ['default:fence_wood'] = 4,
        ['default:fence_junglewood'] = 4,
        ['default:fence_pine_wood'] = 4,
        ['default:fence_acacia_wood'] = 4,
        ['default:fence_aspen_wood'] = 4,
        -- Puertas cerramientos
        ['doors:gate_wood_open'] = 5,
        ['doors:gate_wood_closed'] = 5,
        ['doors:gate_junglewood_open'] = 5,
        ['doors:gate_junglewood_closed'] = 5,
        ['doors:gate_acacia_wood_open'] = 5,
        ['doors:gate_acacia_wood_closed'] = 5,
        ['doors:gate_aspen_wood_open'] = 5,
        ['doors:gate_aspen_wood_closed'] = 5,
        ['doors:gate_pine_wood_open'] = 5,
        ['doors:gate_pine_wood_closed'] = 5,
        -- Verjas
        ['xpanes:bar'] = 6,
        ['xpanes:bar_flat'] = 6,
        -- Escaleras
        ['default:ladder_steel'] = 7,
        ['default:ladder_wood'] = 7,
        -- Trampillas
        ['doors:trapdoor_steel'] = 8,
        ['doors:trapdoor_steel_open'] = 8,
        ['doors:trapdoor'] = 8,
        ['doors:trapdoor_open'] = 8,
        -- Antorcha
        ['default:torch'] = 9,
        ['default:torch_wall'] = 9
        -- Losas "stairs:slab_*" y escalones "stairs:stair_*" en el código
      },


   -- Exporta un trozo de mapa comenzando en el bloque (ax1, ay1, az1) y terminando en el (ax2, ay2, az2)
   exportabloques = function(athis, anombrefichero, ax1, ay1, az1, ax2, ay2, az2)

      -- Escritura del mapa en formato openscad
      local ruta = minetest.get_worldpath() .. "/" .. anombrefichero;
      local destino, emsg = io.open(ruta, "w");
      if not destino then
         error(emsg);
      end

      -- Cabecera y módulos del fichero .scad
      destino:write(string.format('// Mapa de [%d, %d, %d] a [%d, %d, %d]\n\n', ax1, ay1, az1, ax2, ay2, az2));
      destino:write('module bloque(x,y,z,lon) { translate([x, y, z]) cube([1.001, lon + .001, 1.001]); }\n\n');
      destino:write('module losa(x,y,z,p2) {\n');
      destino:write(' if ((p2==0)||(p2==1)||(p2==2)||(p2==3)) translate([x, y, z]) cube([1.001, 1.001, 0.501]);\n');
      destino:write(' if ((p2==4)||(p2==5)||(p2==6)||(p2==7)) translate([x, y, z]) cube([1.001, 0.501, 1.001]);\n');
      destino:write(' if ((p2==8)||(p2==9)||(p2==10)||(p2==11)) translate([x, y+0.5, z]) cube([1.001, 0.501, 1.001]);\n');
      destino:write(' if ((p2==12)||(p2==13)||(p2==14)||(p2==15)) translate([x, y, z]) cube([0.501, 1.001, 1.001]);\n');
      destino:write(' if ((p2==16)||(p2==17)||(p2==18)||(p2==19)) translate([x+0.5, y, z]) cube([0.501, 1.001, 1.001]);\n');
      destino:write(' if ((p2==20)||(p2==21)||(p2==22)||(p2==23)) translate([x, y, z+0.5]) cube([1.001, 1.001, 0.501]); }\n\n');
      destino:write('module escalon(x,y,z,p2) {\n');
      destino:write(' losa(x,y,z,p2);\n');
      destino:write(' if ((p2==0)||(p2==6)) translate([x, y+0.5, z+0.5]) cube([1.001, 0.501, 0.501]);\n');
      destino:write(' if ((p2==1)||(p2==15)) translate([x+0.5, y, z+0.5]) cube([0.501, 1.001, 0.501]);\n');
      destino:write(' if ((p2==2)||(p2==8)) translate([x, y, z+0.5]) cube([1.001, 0.501, 0.501]);\n');
      destino:write(' if ((p2==3)||(p2==17)) translate([x, y, z+0.5]) cube([0.501, 1.001, 0.501]);\n');
      destino:write(' if ((p2==16)||(p2==7)) translate([x, y+0.5, z]) cube([0.501, 0.501, 1.001]);\n');
      destino:write(' if ((p2==18)||(p2==11)) translate([x, y, z]) cube([0.501, 0.501, 1.001]);\n');
      destino:write(' if ((p2==9)||(p2==14)) translate([x+0.5, y, z]) cube([0.501, 0.501, 1.001]);\n');
      destino:write(' if ((p2==5)||(p2==12)) translate([x+0.5, y+0.5, z]) cube([0.501, 0.501, 1.001]);\n');
      destino:write(' if ((p2==20)||(p2==4)) translate([x, y+0.5, z]) cube([1.001, 0.501, 0.501]);\n');
      destino:write(' if ((p2==21)||(p2==19)) translate([x, y, z]) cube([0.501, 1.001, 0.501]);\n');
      destino:write(' if ((p2==22)||(p2==10)) translate([x, y, z]) cube([1.001, 0.501, 0.501]);\n');
      destino:write(' if ((p2==23)||(p2==13)) translate([x+0.5, y, z]) cube([0.501, 1.001, 0.501]); }\n\n');
      destino:write('module muro(x,y,z,con) {\n');
      destino:write(' translate([x+0.333, y+0.333, z]) cube([0.334, 0.334, 1.001]);\n');
      destino:write(' if ((con==1)||(con==3)||(con==5)||(con==7)||(con==9)||(con==11)||(con==13)||(con==15)) translate([x+0.375, y+0.666, z]) cube([0.251, 0.334, 0.66]);\n');
      destino:write(' if ((con==2)||(con==3)||(con==6)||(con==7)||(con==10)||(con==11)||(con==14)||(con==15)) translate([x+0.666, y+0.375, z]) cube([0.334, 0.251, 0.66]);\n');
      destino:write(' if ((con==4)||(con==5)||(con==6)||(con==7)||(con==12)||(con==13)||(con==14)||(con==15)) translate([x+0.375, y, z]) cube([0.251, 0.334, 0.66]);\n');
      destino:write(' if ((con==8)||(con==9)||(con==10)||(con==11)||(con==12)||(con==13)||(con==14)||(con==15)) translate([x, y+0.375, z]) cube([0.334, 0.251, 0.66]);}\n\n');
      destino:write('module valla(x,y,z,con) {\n');
      destino:write(' translate([x+0.375, y+0.375, z]) cube([0.251, 0.251, 1.001]);\n');
      destino:write(' if ((con==1)||(con==3)||(con==5)||(con==7)||(con==9)||(con==11)||(con==13)||(con==15)) {\n');
      destino:write('  translate([x+0.42, y+0.625, z+0.2]) cube([0.16, 0.376, 0.16]);\n');
      destino:write('  translate([x+0.42, y+0.625, z+0.6]) cube([0.16, 0.376, 0.16]);}\n');
      destino:write(' if ((con==2)||(con==3)||(con==6)||(con==7)||(con==10)||(con==11)||(con==14)||(con==15)) {\n');
      destino:write('  translate([x+0.625, y+0.42, z+0.2]) cube([0.376, 0.16, 0.16]);\n');
      destino:write('  translate([x+0.625, y+0.42, z+0.6]) cube([0.376, 0.16, 0.16]);}\n');
      destino:write(' if ((con==4)||(con==5)||(con==6)||(con==7)||(con==12)||(con==13)||(con==14)||(con==15)) {\n');
      destino:write('  translate([x+0.42, y, z+0.2]) cube([0.16, 0.376, 0.16]);\n');
      destino:write('  translate([x+0.42, y, z+0.6]) cube([0.16, 0.376, 0.16]);}\n');
      destino:write(' if ((con==8)||(con==9)||(con==10)||(con==11)||(con==12)||(con==13)||(con==14)||(con==15)) {\n');
      destino:write('  translate([x, y+0.42, z+0.2]) cube([0.376, 0.16, 0.16]);\n');
      destino:write('  translate([x, y+0.42, z+0.6]) cube([0.376, 0.16, 0.16]);}}\n\n');
      destino:write('module escalerabasica() { union() {\n');
      destino:write(' translate([0.4, -0.4, 0]) cylinder(r=0.1, h=1.01);\n');
      destino:write(' translate([0.4, 0.4, 0]) cylinder(r=0.1, h=1.01);\n');
      destino:write(' translate([0.4, 0.5, 0.2]) rotate([90, 0, 0]) cylinder(r=0.05, h=1.01);\n');
      destino:write(' translate([0.4, 0.5, 0.45]) rotate([90, 0, 0]) cylinder(r=0.05, h=1.01);\n');
      destino:write(' translate([0.4, 0.5, 0.70]) rotate([90, 0, 0]) cylinder(r=0.05, h=1.01);\n');
      destino:write(' translate([0.4, 0.5, 0.95]) rotate([90, 0, 0]) cylinder(r=0.05, h=1.01); } }\n\n');
      destino:write('module escalera(x,y,z,p2) { $fn=8;\n');
      destino:write(' if (p2 == 2) translate([x+0.5, y+0.5, z]) rotate([0, 0, 0]) escalerabasica();\n');
      destino:write(' if (p2 == 3) translate([x+0.5, y+0.5, z]) rotate([0, 0, 180]) escalerabasica();\n');
      destino:write(' if (p2 == 4) translate([x+0.5, y+0.5, z]) rotate([0, 0, 90]) escalerabasica();\n');
      destino:write(' if (p2 == 5) translate([x+0.5, y+0.5, z]) rotate([0, 0, 270]) escalerabasica(); }\n\n');
      destino:write('module trampabasica(a,b) { rotate([b, 0, a]) translate([-0.5, -0.5, -0.5]) difference() {\n');
      destino:write(' cube([1.001, 0.1, 1.001]);\n');
      destino:write(' translate([0.1, -0.1, 0.1]) cube([0.8, 0.12, 0.8]);\n');
      destino:write(' translate([0.1, 0.08, 0.1]) cube([0.8, 0.1, 0.8]);\n');
      destino:write(' translate([0.2, -0.1, 0.2]) cube([0.2, 0.2, 0.3]);\n');
      destino:write(' translate([0.2, -0.1, 0.6]) cube([0.2, 0.2, 0.3]);\n');
      destino:write(' translate([0.6, -0.1, 0.2]) cube([0.2, 0.2, 0.3]);\n');
      destino:write(' translate([0.6, -0.1, 0.6]) cube([0.2, 0.2, 0.3]);} }\n\n');
      destino:write('module trampa(x,y,z,p2) { translate([x+0.5, y+0.5, z+0.5]) {\n');
      destino:write(' if (p2 == -1) trampabasica(0, 90);\n');
      destino:write(' if (p2 == 0) trampabasica(180, 0);\n');
      destino:write(' if (p2 == 1) trampabasica(90, 0);\n');
      destino:write(' if (p2 == 2) trampabasica(0, 0);\n');
      destino:write(' if (p2 == 3) translate([x+0.1, y, z]) rotate([0, -90, 0]) trampabasica(); } }\n\n');
      destino:write('module antorchabasica(a,b) { $fn=8; union() {\n');
      destino:write(' rotate([a, b, 0]) cylinder(r=0.08, h=0.6);\n');
      destino:write(' sphere(0.15);\n');
      destino:write(' cylinder(r1=0.125, r2=0, h=0.35);} }\n\n');
      destino:write('module antorcha(x,y,z,p2) {\n');
      destino:write(' if (p2 == 1) translate([x+0.5, y+0.5, z+0.6]) antorchabasica(0,180);\n');
      destino:write(' if (p2 == 2) translate([x+0.7, y+0.5, z+0.6]) antorchabasica(0,145);\n');
      destino:write(' if (p2 == 3) translate([x+0.3, y+0.5, z+0.6]) antorchabasica(0,-145);\n');
      destino:write(' if (p2 == 4) translate([x+0.5, y+0.7, z+0.6]) antorchabasica(-145,0);\n');
      destino:write(' if (p2 == 5) translate([x+0.5, y+0.3, z+0.6]) antorchabasica(145,0); }\n\n');
      destino:write('module puertabasica(a) { rotate([0, 0, a]) translate([-0.5, -0.4, 0]) difference() {\n');
      destino:write(' cube([1.001, 0.1, 2.001]);\n');
      destino:write(' translate([0.1, -0.1, 0.1]) cube([0.8, 0.12, 0.8]);\n');
      destino:write(' translate([0.1, 0.08, 0.1]) cube([0.8, 0.1, 0.8]);\n');
      destino:write(' translate([0.1, -0.1, 1.1]) cube([0.8, 0.12, 0.8]);\n');
      destino:write(' translate([0.1, 0.08, 1.1]) cube([0.8, 0.1, 0.8]);\n');
      destino:write(' translate([0.2, -0.1, 1.2]) cube([0.2, 0.2, 0.3]);\n');
      destino:write(' translate([0.2, -0.1, 1.6]) cube([0.2, 0.2, 0.3]);\n');
      destino:write(' translate([0.6, -0.1, 1.2]) cube([0.2, 0.2, 0.3]);\n');
      destino:write(' translate([0.6, -0.1, 1.6]) cube([0.2, 0.2, 0.3]); } }\n\n');
      destino:write('module puerta(x,y,z,p2) { translate([x+0.5, y+0.5, z]) {\n');
      destino:write(' if (p2 == 0) puertabasica(0);\n');
      destino:write(' if (p2 == 1) puertabasica(-90);\n');
      destino:write(' if (p2 == 2) puertabasica(180);\n');
      destino:write(' if (p2 == 3) puertabasica(90); } }\n\n');
      destino:write('module portonbasico(a,b) { rotate([0, 0, a]) {\n');
      destino:write(' translate([-0.625, -0.125, 0]) cube([0.251, 0.251, 1.001]);\n');
      destino:write(' translate([0.375, -0.125, 0]) cube([0.251, 0.251, 1.001]);\n');
      destino:write(' translate([-0.5, -0.08, 0.6]) rotate([0, 0, b == 0 ? -90 : 0]) cube([1, 0.16, 0.16]);\n');
      destino:write(' translate([-0.5, -0.08, 0.1]) rotate([0, -30 ,b == 0 ? -90 : 0]) cube([1, 0.16, 0.16]); } }\n\n');
      destino:write('module porton(x,y,z,p2,a) { translate([x+0.5, y+0.5, z]) {\n');
      destino:write(' if (p2 == 0) portonbasico(0,a);\n');
      destino:write(' if (p2 == 1) portonbasico(-90,a);\n');
      destino:write(' if (p2 == 2) portonbasico(180,a);\n');
      destino:write(' if (p2 == 3) portonbasico(90,a); } }\n\n');
      destino:write('module verjabasico(r) {rotate([0,0,r]) translate([-0.05, -0.05, 0]) difference() {\n');
      destino:write(' cube([0.551, 0.1, 1.001]);\n');
      destino:write(' translate([0.05, -0.05, 0.05]) cube([0.18, 0.2, 0.801]);\n');
      destino:write(' translate([0.35, -0.05, 0.05]) cube([0.18, 0.2, 0.801]); } }\n\n');
      destino:write('module verja(x,y,z,con) { translate([x+0.5, y+0.5, z]) {\n');
      destino:write(' if ((con==1)||(con==3)||(con==5)||(con==7)||(con==9)||(con==11)||(con==13)||(con==15)) verjabasico(90);\n');
      destino:write(' if ((con==2)||(con==3)||(con==6)||(con==7)||(con==10)||(con==11)||(con==14)||(con==15)) verjabasico(0);\n');
      destino:write(' if ((con==4)||(con==5)||(con==6)||(con==7)||(con==12)||(con==13)||(con==14)||(con==15)) verjabasico(-90);\n');
      destino:write(' if ((con==8)||(con==9)||(con==10)||(con==11)||(con==12)||(con==13)||(con==14)||(con==15)) verjabasico(180); } }\n\n');
      destino:write('union() {\n');

      -- Manipuladores de nodos contenidos entre las coordenadas indicadas
      local vm = minetest.get_voxel_manip();
      local pMin, pMax = vm:read_from_map({ x = ax1, y = ay1, z = az1 }, { x = ax2, y = ay2, z = az2 });
      local data = vm:get_data();
      local par2 = vm:get_param2_data();
      local va = VoxelArea:new({ MinEdge = pMin, MaxEdge = pMax });

      -- Comprueba conexión de muros
      local function muro_vecino(x, y, z)
         if va:contains(x, y, z) then
            local ind = va:index(x, y, z);
            local nid = data[ind];
            local nom = minetest.get_name_from_content_id(nid);
            local val = athis.bloquesadmitidos[nom];
            if (val == 2) or (string.find(nom, 'stone') ~= nil) or (string.find(nom, 'cobble') ~= nil) then
               return true;
            end
         end
         return false;
      end

      -- Comprueba conexión de vallas
      local function valla_vecina(x, y, z)
         if va:contains(x, y, z) then
            local ind = va:index(x, y, z);
            local nid = data[ind];
            local nom = minetest.get_name_from_content_id(nid);
            local val = athis.bloquesadmitidos[nom];
            if (val == 4) or (val == 5) then
               return true;
            end
         end
         return false;
      end

       -- Comprueba conexión de verjas
      local function verja_vecina(x, y, z)
         if va:contains(x, y, z) then
            local ind = va:index(x, y, z);
            local nid = data[ind];
            local nom = minetest.get_name_from_content_id(nid);
            local val = athis.bloquesadmitidos[nom];
            if (val == 6) then
               return true;
            end
         end
         return false;
      end

      -- Exploración de bloques a exportar
      for ly = ay1, ay2 do
         local oz = ly - ay1;
         for lx = ax1, ax2 do
            local ox = lx - ax1;
            local longitud = 0;
            local inicio = 0;
            for lz = az1, az2 do
               local oy = lz - az1;
               local indice = va:index(lx, ly, lz);
               local nodo_id = data[indice];
               local nodo_nom = minetest.get_name_from_content_id(nodo_id);
               local nodo_p2 = par2[indice];
               local valor = athis.bloquesadmitidos[nodo_nom];

               -- Agrupar bloques para simplificar modelo
               if valor == 1 then
                  if longitud == 0 then inicio = oy; end
                  longitud = longitud + 1;
               else
                  if longitud > 0 then
                     destino:write(string.format(' bloque(%d, %d, %d, %d);\n', ox, inicio, oz, longitud));
                     longitud = 0;
                  end

                  local vecinos = 0;
                  local subn = string.sub(nodo_nom, 8, 12);

                  if subn == 'slab_' then
                     destino:write(string.format(' losa(%d, %d, %d, %d);\n', ox, oy, oz, nodo_p2));

                  elseif subn == 'stair' then
                     destino:write(string.format(' escalon(%d, %d, %d, %d);\n', ox, oy, oz, nodo_p2));

                  elseif valor == 2 then
                     if muro_vecino(lx, ly, lz+1) then vecinos = vecinos  + 1; end
                     if muro_vecino(lx+1, ly, lz) then vecinos = vecinos  + 2; end
                     if muro_vecino(lx, ly, lz-1) then vecinos = vecinos  + 4; end
                     if muro_vecino(lx-1, ly, lz) then vecinos = vecinos  + 8; end
                     destino:write(string.format(' muro(%d, %d, %d, %d);\n', ox, oy, oz, vecinos));

                  elseif valor == 3 then
                     destino:write(string.format(' puerta(%d, %d, %d, %d);\n', ox, oy, oz, nodo_p2));

                  elseif valor == 4 then
                     if valla_vecina(lx, ly, lz+1) then vecinos = vecinos  + 1; end
                     if valla_vecina(lx+1, ly, lz) then vecinos = vecinos  + 2; end
                     if valla_vecina(lx, ly, lz-1) then vecinos = vecinos  + 4; end
                     if valla_vecina(lx-1, ly, lz) then vecinos = vecinos  + 8; end
                     destino:write(string.format(' valla(%d, %d, %d, %d);\n', ox, oy, oz, vecinos));

                  elseif valor == 5 then
                     local cerrado = 1
                     if (string.find(nodo_nom, '_closed') == nil) then cerrado = 0; end
                     destino:write(string.format(' porton(%d, %d, %d, %d, %d);\n', ox, oy, oz, nodo_p2, cerrado));

                  elseif valor == 6 then
                     if (string.find(nodo_nom, '_flat') == nil) then
                        if verja_vecina(lx, ly, lz+1) then vecinos = vecinos  + 1; end
                        if verja_vecina(lx+1, ly, lz) then vecinos = vecinos  + 2; end
                        if verja_vecina(lx, ly, lz-1) then vecinos = vecinos  + 4; end
                        if verja_vecina(lx-1, ly, lz) then vecinos = vecinos  + 8; end
                     else
                        vecinos = 20 + nodo_p2;
                     end
                     destino:write(string.format(' verja(%d, %d, %d, %d);\n', ox, oy, oz, vecinos));

                  elseif valor == 7 then
                     destino:write(string.format(' escalera(%d, %d, %d, %d);\n', ox, oy, oz, nodo_p2));

                  elseif valor == 8 then
                     if (string.find(nodo_nom, 'open') == nil) then nodo_p2 = -1; end
                     destino:write(string.format(' trampa(%d, %d, %d, %d);\n', ox, oy, oz, nodo_p2));

                  elseif valor == 9 then
                     destino:write(string.format(' antorcha(%d, %d, %d, %d);\n', ox, oy, oz, nodo_p2));

                  elseif athis.bloquesignorar[nodo_nom] == nil then
                     destino:write(string.format(' // Nodo(%d, %d, %d) [%s], p2:%d);\n', ox, oy, oz, nodo_nom, nodo_p2));
                  end
               end
            end
            if longitud > 0 then
                destino:write(string.format(' bloque(%d, %d, %d, %d);\n', ox, inicio, oz, longitud));
            end
         end
      end
      destino:write('}\n');
      destino:close();
   end
}


-- Registro de privilegio específico
minetest.register_privilege(
   "exportar",
   {
      description = "Permite exportar mapas a fichero",
      give_to_singleplayer = true
   });


-- Registro del comando del chat
minetest.register_chatcommand(
   "openscad",
   {
      params = "x1 y1 z1 x2 y2 z2 [fichero]",
      description = "Exporta mapa definido por el cubo (x1,y1,z1) a (x2,y2,z2) en formato OpenSCAD.",
      privs = { exportar = true },
      func =
         function(nombre, parametros)
            local lp = string.split(parametros, " ")
            local x1 = tonumber(lp[1]);
            local y1 = tonumber(lp[2]);
            local z1 = tonumber(lp[3]);
            local x2 = tonumber(lp[4]);
            local y2 = tonumber(lp[5]);
            local z2 = tonumber(lp[6]);
            local nf = lp[7];

            if z2 == nil then
               return false, "Error en número de parámetros.";
            end

            -- Nombre del fichero
            if nf == nil then
               nf = nombre .. ".scad";
            else
               nf = nf .. ".scad";
            end

            -- Comprueba e intercambia las coordenadas invertidas
            local cambio
            if x1 > x2 then
                 cambio = x2;
                x2 = x1;
                x1 = cambio;
             end
            if y1 > y2 then
               cambio = y2;
               y2 = y1;
               y1 = cambio;
            end
            if z1 > z2 then
               cambio = z2;
               z2 = z1;
               z1 = cambio;
            end

            -- Comprueba límites antes de exportar
            if (x2 - x1) > 150 then
               return false, string.format("Error en longitud X (%d -> %d) > 150.", x1, x2);
            elseif (z2 - z1) > 150 then
               return false, string.format("Error en longitud Z (%d -> %d) > 150.", z1, z2);
            elseif (y2 - y1) > 150 then
               return false, string.format("Error en altura Y (%d -> %d) > 150.", y1, y2);
            else
               exportanodos:exportabloques(nf, x1, y1, z1, x2, y2, z2);
               return true, "Exportado a " .. nf;
            end
         end
   });

Configuración específica de minetest

Para empezar lo ideal es partir de una superficie lo más limpia y plana posible, donde se fácil empezar a construir, y donde tengamos más libertad de movimientos (como “volar”) para ir depositando los bloques que conformarán nuestra construcción a imprimir. Tampoco nos interesa que “pase el tiempo” y se nos haga de noche.

NOTA: en las nuevas veriosnes de minetest > 4.15 es posible ajustar muchos de estos parámetros desde los paneles de “Configuración” → “Configuración avanzada”

Edita el fichero “/home/alumno/.minetest/minetest.conf” y añade/ajusta las siguientes variables:

cinematic = false
creative_mode = true
enable_3d_clouds = false
enable_damage = false
enable_particles = false
enable_shaders = false
fixed_map_seed = 81072245
leaves_style = opaque
mg_name = v6
mg_flags = flat
name =
server_dedicated = false
smooth_lighting = false
free_move = true
fast_move = true
no_clip = true
doubletap_jump = true
time_speed = 0

Creación de un mundo “plano” y selección del “mod”

Abre de nuevo el programa y verifica que el módulo que hemos creado aparece disponible en la solapa “Mods”:

En la solapa “Un jugador” activa la opción “Modo creativo” y desactiva “Permitir daños”. A continuación pulsa sobre “Nuevo” para crear un nuevo mapa.

Dale un nombre al “mundo”. Opcionalmente dale una semilla al generador (81072245 funciona bien). Utiliza la versión “Flat(o en su defecto «V6«) y el tipo de juego “Minetest”. Prueba otras combinaciones.  Pulsa el botón “Crear” para continuar:

De vuelta a la pantalla de “Un Jugador” selecciona el mundo recién creado y pulsa sobre “Configurar”:

Pulsa sobre el módulo “openscad” que hemos creado y asegúrate de marcar la casilla “Activado” para que se utilice en este mundo en particular. Después pulsa en “Guardar”.

De vuelta a la pantalla de “Un Jugador” ya puedes seleccionar el mundo y pulsar sobre el botón “Jugar”:

Si todo ha ido bien deberías ver una pantalla muy similar a esta:

Apunta con el ratón y muévete con las teclas:

W – Adelante
S – Atrás
A – Izquierda
D – Derecha
Espacio – Saltar
K – Volar (si/no)
J – Ir rápido (si/no)
H – Atravesar paredes (si/no)

Para que funcionen las tres últimas acciones hay que “dar permiso” al jugador. Pulsa la tecla F10 y escribe en la consola:

 /grant singleplayer fly
 /grant singleplayer fast
 /grant singleplayer all

Para salir de la consola pulsa de nuevo F10.

Los controles de vuelo son

Espacio – para ascender
Shif(mayúsculas) – para descender

NOTA: si juegas en un servidor será el administrador el que tenga que darte los permisos sustituyendo “singleplayer” por tu nombre de jugador.

Configuración de los bloques (materiales) a utilizar

Aunque en el script del módulo se ha procurado incluir la mayoría de los materiales «imprimibles» (el agua, fuego, lava, el cristal, árboles, flores y demás elementos decorativos no se contemplan como tal) vamos a procurar utilizar sólo los materiales de tipo “bloque”. Te recomiendo empezar con los siguientes tipos:

  • dirt – Para la tierra
  • dirt_with_grass – Para la superficie sobre la que construir
  • stone – Para la construcción

Abre el inventario con la letra “i”, busca estos tipos de bloque (páginas 2 y 5) y colócalos en la rejilla central. Pulsa la “i” de nuevo para salir.

NOTA: si dejas el cursor encima de un bloque te mostrará el nombre de éste.

Creación del modelo

Selecciona el material a utilizar con las teclas 1, 2 y 3. Pulsa con el botón derecho del ratón para depositar un bloque, y con el izquierdo para destruirlo.

Para indicar dónde aparecerá el bloque o cual va a ser destruido utiliza la cruz central de la pantalla. Esta “marcará” las caras del bloque señalado. Asegúrate de tener bien apuntada la cara donde se “pegará” en bloque que vayas a crear.

TRUCOS y NOTAS:

  • Puedes empezar dibujando en el suelo una base sobre la que levantar el resto de la estructura y que delimite su extensión (máximo 150 bloques de lado).
  • Si dejas el botón del ratón pulsado se repite la acción (poner/destruir) hasta que lo sueltas, lo que te permite diseñar más rápidamente.
  • Cierra bien las esquinas y los «tejados» (evita bloques que se «toquen» sólo por los vértices -> deben estar «pegados» por las caras).
  • Existen complementos (módulos) gratuitos que puedes añadir a tu minetest para mejorar las tareas de construcción (copiar/cortar/pegar/repetir grupos de bloqes…), como “worldEdit”:
    https://forum.minetest.net/viewtopic.php?id=572
  • Cuidado con los puentes, puertas, ventanas, balcones… Las impresoras 3D no pueden imprimir en el aire. Cualquier hueco de más de tres bloques o voladizo de más de 1 bloque podría imprimirse mal y arruinar el modelo.
  • Céntrate en el exterior del modelo. No pierdas tiempo diseñando interiores que no se van a ver.
  • Recuerda que por ahora imprimimos en 1 sólo color, por lo que las decoraciones con bloques de distintos tonos y/o colores no se mostrarán como tal en la pieza impresa!

Uso del módulo y exportación

Coordenadas

El mundo de Minetest es un gran cubo. Y debido a esto, una posición en el mundo puede serww fácilmente expresada con coordenadas cartesianas. Es decir, para cada posición en el mundo, hay 3 valores: X, Y y Z.

Las coordenadas se expresan así: (5, 45, -12). Esto se refiere a la posición donde X = 5, Y = 45 y Z = -12. Las 3 letras se llaman «ejes»: Y es para la altura. X y Z son para la posición horizontal. El tamaño de un bloque es 1 unidad.

Para exportar nuestro modelo tenemos que averiguar desde que esquina a que esquina opuesta están localizados nuestros bloques. Para “recortar” también el suelo recuerda reducir 1 o 2 los valores de la Y inferior.

Sitúate cerca de una esquina de tu construcción:

  1. Pulsa la tecla F7 hasta que se muestre a tu personaje. Asegúrate de estar a la altura del suelo.
  2. Pulsa la tecla F5 para mostrar los mensajes del depurador: Al principio de la segunda línea de datos que aparece en la parte superior de la pantalla podrás ver las coordenadas de tu personaje en el mundo.
  3. En mi caso marca (-0.8,  3.5,  -7.2). Este será mi punto (A). Como quiero coger 1 fila de base: Y:  3.5 – 1 = 2.5. Apúntalo para más adelante.

Sitúate ahora en la esquina opuesta del rectángulo imaginario que abarque tu construcción. Apunta de nuevo tus coordenadas.

En mi caso el punto (B) sería (-14.2,  3.5,  7.2). Como he construido 6 líneas de bloques por encima del suelo añadiré por lo menos 6 al valor de la Y:  3.5 + 6 = 9.5. (puedes ser generoso/a, el «aire» por encima no se imprime).

Me quedan:

A =   -0.8,  2.5,  -7.2
B = -14.2,  9.5,   7.2

Con estos datos ejecuta el comando que hemos definido en nuestro mod: pulsa la tecla F10 y escribe:

/openscad -0.8  2.5  -7.2  -14.2  9.5  7.2

El fichero generado se habrá guardado en la carpeta “del mundo” actual. Opcionalmente puedes pasarle como último parámetro el nombre del fichero a exportar (sin extensión). En caso contrario tomará el nombre del usuario que invoca el script.

En mi caso y como se aprecia en la figura en la carpeta “.minetest/worlds” de mi usuario:

.minetest/worlds/
     └── Castillo de David/
         ├── auth.txt
         ├── env_meta.txt
         ├── force_loaded.txt
         ├── ipban.txt
         ├── map_meta.txt
         ├── map.sqlite
         ├── singleplayer.scad
         ├── players/
         │   └── singleplayer
         └── world.mt

NOTAS:

  • Los decimales en las coordenadas se deben a la posición parcial del usuario sobre un bloque. Puedes redondear los valores al entero mayor que les corresponda.

  • Si sales y vuelves a entrar al mundo asegúrate de recorrer de nuevo todo el espacio antes de exportar para que minetest lo regenere internamente o el modelo puede aparecer incompleto.

Generar e imprimir el modelo 3D

Generar el modelo STL 3D final

Invoca el programa openscad pasándole como parámetro la ruta del fichero generado, o bien utiliza su menú “Archivo” → “Abrir”.

Pulsa a continuación la tecla F6 (o menú “Diseñar” → “Render”) para obtener un modelo sólido. Cuando termine el proceso utiliza el menú “Archivo” → “Expoportar” → “Exportar como STL

Generar el g-code para la impresora

Abre ahora el fichero STL anterior con el programa “Slic3r” o “Cura”. Carga la configuración para tu modelo de impresora y filamento y genera el fichero .gcode a enviar a la impresora.

Resultado

Con el modelo del ejemplo y tras 25 minutos de impresión este fue el resultado final (y no, no imprimimos monedas, es para ver la escala):

.. y estos fueron los de los chicos….

Sugerencias para prácticas en casa o en clase:

  • Modelado e impresión de las iglesias y/o monumentos más representativos de tu ciudad
  • Diseño e impresión por piezas de una villa romana, ¡o de Roma!
  • Lo mismo para la Acrópolis de Atenas
  • Las “arenas” del juego Clash Royale
  • Llaveros con forma de “chapa” y con tu nombre
  • Un plano en relieve para invidentes de tu colegio, chapas con los nombres de las clases, laboratorios, biblioteca, secretaría….
  • Laberintos por el que luego pueda moverse un pequeño rodamiento (bola, canica) para jugar
  • Las piezas del juego del ajedrez
  • Soportes para tabletas o móviles
  • ….

Trabaja si puedes por equipos sobre un mismo mundo (servidor). Es mucho más divertido!

Notas sobre el módulo que exporta a openSCAD:
Se trata de una primera reversión que habrá que seguir mejorando para que incluya otro tipo de bloques como vegetación, mobiliario, objetos….

Por ahora se ha mejorado la entrada de parámetros, añadidos algunos controles para evitar errores, limitada la exportación a un cubo de 150 bloques de lado, reconocido bloques especiales como antorchas, escalones, vallas, escaleras, puertas, trampillas.. en sus diferentes posiciones. Por último se ha establecido un permiso especial llamado «exportar» para poder utilizar el módulo por parte de un jugador.

Cuanto más grande y complejo sea el mapa a exportar más le costará luego a openSCAD hacer los cálculos. ¡Paciencia!