scieee AI-readable full text Open interactive document viewer

Uso de Blender a través de la programación en la Ingeniería de Caminos, Canales y Puertos

Puech de Oriol, Miguel

Abstract

Este documento introduce la posibilidad de utilizar Blender en la Ingeniería de Caminos, Canales y Puertos como herramienta de diseño 3D a través de la programación de utilidades en Python y mediante la programación visual con árboles de nodos. Para ello,se han creado cinco herramientas que demuestran las capacidades que un programa como Blender, creado para un ámbito ajeno al mundo de la ingeniería, tiene para un ingeniero relacionado con la construcción civil. Las cinco herramientas creadas incluyen aspectos de Caminos, Canales y Puertos, para comprobar la viabilidad del uso de este software en el sector. Estas utilidades permiten al usuario: - Caminos: realizar modelos tridimensionales del terreno y extraer perfiles longitudinales de carreteras, - Canales: crear tuberías normalizadas para distintos materiales, - Puertos: crear rocas naturales y bloques de hormigón artificial para diques. Los códigos desarrollados se pueden ejecutar de forma directa en Blender, y en el caso de la utilidad desarrollada para la construcción de tuberías es posible su instalación como un complemento, lo que demuestra la capacidad prácticamente ilimitada de personalización del entorno de trabajo. Para una mejor comprensión de su programación, se hace una introducción a la creación de objetos, mallas, materiales, modificadores, texturas y otros elementos de modelado existentes en Blender. Se ha podido comprobar a través de las herramientas creadas: - la capacidad de utilizar distintas fuentes de datos, - la capacidad de realizar modelos 3D y 2D indistintamente, - la versatilidad en el desarrollo de los modelos, - la capacidad de automatización de procesos, - la posibilidad de obtener resultados fotorrealistas aplicando materiales a los objetos, - la capacidad de generar elementos de geometría conocida mediante la parametrización de estos - y modelos de geometría imperfecta y aleatoria mediante generación procedimental. Finalmente, hay que indicar que la existencia de complementos realizados por terceros permite ampliar las capacidades del programa sin la necesidad de tener conocimientos de programación y, todo esto, a través de un programa completamente gratuito.

Full text

I Departamento de Ingeniería Gráfica Sevilla, 2021 Autor: Miguel Puech de Oriol Tutora: Cristina Torrecillas Lozano Uso de Blender a través de la programación en la Ingeniería de Caminos, Canales y Puertos Trabajo Fin de Máster Ingeniería de Caminos, Canales y Puertos II III Proyecto Fin de Máster Ingeniería de Caminos, Canales y Puertos Uso de Blender a través de la programación en la Ingeniería de Caminos, Canales y Puertos Autor: Miguel Puech de Oriol Tutora: Cristina Torrecillas Lozano Profesora Titular de Universidad Departamento de Ingeniería Gráfica Escuela Técnica Superior de Ingeniería Universidad de Sevilla Sevilla, 2021 IV V Proyecto Fin de Máster: Uso de Blender a través de la programación en la Ingeniería de Caminos, Canales y Puertos Autor: Miguel Puech de Oriol Tutora: Cristina Torrecillas Lozano El tribunal nombrado para juzgar el Proyecto arriba indicado, compuesto por los siguientes miembros: Presidente: Vocales: Secretario: Acuerdan otorgarle la calificación de: Sevilla, 2021 El Secretario del Tribunal VI VII Resumen Este documento introduce la posibilidad de utilizar Blender en la Ingeniería de Caminos, Canales y Puertos como herramienta de diseño 3D a través de la programación de utilidades en Python y mediante la programación visual con árboles de nodos. Para ello, se han creado cinco herramientas que demuestran las capacidades que un programa como Blender, creado para un ámbito ajeno al mundo de la ingeniería, tiene para un ingeniero relacionado con la construcción civil. Las cinco herramientas creadas incluyen aspectos de Caminos, Canales y Puertos, para comprobar la viabilidad del uso de este software en el sector. Estas utilidades permiten al usuario: - Caminos: realizar modelos tridimensionales del terreno y extraer perfiles longitudinales de carreteras, - Canales: crear tuberías normalizadas para distintos materiales, - Puertos: crear rocas naturales y bloques de hormigón artificial para diques. Los códigos desarrollados se pueden ejecutar de forma directa en Blender, y en el caso de la utilidad desarrollada para la construcción de tuberías es posible su instalación como un complemento, lo que demuestra la capacidad prácticamente ilimitada de personalización del entorno de trabajo. Para una mejor comprensión de su programación, se hace una introducción a la creación de objetos, mallas, materiales, modificadores, texturas y otros elementos de modelado existentes en Blender. Se ha podido comprobar a través de las herramientas creadas: - la capacidad de utilizar distintas fuentes de datos, - la capacidad de realizar modelos 3D y 2D indistintamente, - la versatilidad en el desarrollo de los modelos, - la capacidad de automatización de procesos, - la posibilidad de obtener resultados fotorrealistas aplicando materiales a los objetos, - la capacidad de generar elementos de geometría conocida mediante la parametrización de estos - y modelos de geometría imperfecta y aleatoria mediante generación procedimental. Finalmente, hay que indicar que la existencia de complementos realizados por terceros permite ampliar las capacidades del programa sin la necesidad de tener conocimientos de programación y, todo esto, a través de un programa completamente gratuito. VIII IX Abstract This document introduces the possibility of using Blender in civil engineering as a 3D modelling tool through programming with Python and visual programming with node trees. For this, five tools have been created to demonstrate the capabilities that a program like Blender, created for a foreign field to the world of engineering, has for an engineer. The five created tools include aspects of Roads, Channels and Ports, to prove the viability of using the software in this sector. These utilities allow the user: − Roads: build three-dimensional models of the terrain and extract longitudinal profiles of roads, − Channels: build standardized pipes for different materials, − Ports: build natural rocks and artificial concrete breakwaters blocks. The developed codes can be executed directly in Blender, and in the case of the utility developed to build pipes it is possible to install it as an add-on, which demonstrates the practically unlimited capacity of customization of the working environment. For a better comprehension, an introduction is made of the creation of objects, meshes, materials, modifiers, textures and other modelling Blender elements. It has been possible to prove through the developed tools: − the capability to use different data sources, − the capability to create 3D and 2D models indistinctly, − the versatility in the models' development, − the capability of process automatization, − the possibility to obtain photorealistic results applying materials to the objects, − the capability to generate elements of known geometry through parametrization, − and models of imperfect and random geometry through procedural generation. Finally, it should be noted that the existence of third-party add-ons allows to increase the program capabilities without the need to have any programming knowledge, and all this within a completely free program. 16 1. Introducción 1.1. Introducción al problema La ingeniería hace tiempo que dejo el papel para el diseño y modelado por el Diseño Asistido por Computadora que permiten un desarrollo, creación, modificación, análisis y optimización de los diseños de forma más precisa y eficiente. En este proceso y, como es lógico, en primer lugar, se desarrollaron herramientas de modelado 2D (AutoCAD, LibreCAD., etc.). Más tarde aparecieron las herramientas de modelado 3D (Solid Edge, CATIA, etc.), aunque en la actualidad las herramientas suelen permitir el desarrollo en 2D y 3D indistintamente. La Ingeniería de Caminos, Canales y Puertos no es ajena a este fenómeno, aunque si suele ir por detrás de otras ingenierías en la adopción de estas tecnologías. Así, a día de hoy, el modelado 3D no suele ser utilizado aún en los proyectos en los que la tecnología CAD es utilizada principalmente para la creación de planos, documentos indispensables en los proyectos. Esto no ocurre así en la arquitectura que ha sido capaz de adoptar las nuevas tecnologías de manera mucho más rápida y donde el uso de herramientas 3D es mucho más habitual, como se constata con la adopción del llamado Modelado de Información de Construcción (BIM) que incorpora al diseño 3D, el tratamiento de información. Aunque de adopción tardía el diseño 3D, muestra ser de gran interés en la ingeniería de caminos permitiendo acercar un poco más el diseño realizado a la realidad. Esto es de gran importancia cuando el ingeniero debe trasladar los conceptos a un cliente que puede ser totalmente ajeno a la ingeniería y al que, por tanto, pueda serle difícil la comprensión de un plano bidimensional. Hoy en día incluso se pueden utilizar los diseños tridimensionales para su visualización a través de entornos de realidad aumentada, que permiten la inmersión tanto del ingeniero que está realizando el diseño como del cliente, lo que permite lograr mejores soluciones a los problemas que se está resolviendo, y una mayor participación del cliente en el desarrollo del proyecto llegando a una solución más personalizada del problema. Quizás una de las causas por las que la ingeniería de caminos realiza una adopción tardía de las tecnologías frente a otras ingenierías o la arquitectura, es la complejidad de los proyectos que en ella se realizan, puesto que incorporan componentes de muy distinta índole, y las dimensiones de los proyectos son en general mucho mayores. No es lo mismo el proyecto de construcción de un puerto donde será necesario modelar el agua y su comportamiento en el tiempo, el terreno y su geología, el clima..., y ya dentro del aspecto constructivo, la combinación de piezas naturales como la roca, y artificiales como el hormigón armado, que un proyecto de diseño de una pieza industrial totalmente artificial, que será por lo general fácil de parametrizar y, por tanto, de sencillo diseño en una herramienta CAD. Y esto sin contar con las dimensiones de unos y otros proyectos lo que es ciertamente un problema en los proyectos de ingeniería de caminos ya que no sólo introduce una complejidad, sino un coste computacional muy grande que a veces ya de por sí puede limitar o impedir el diseño 3D. Es muy importante que los programas, además de incorporar el diseño, tengan la capacidad de realizar otras labores propias de la ingeniería de caminos, como el cálculo estructural o hidráulico de la estructura diseñada, de forma directa con el mismo programa de diseño o indirecta por compatibilidad del programa. Además, la capacidad de captar la atención de un cliente o simplemente ayudar al mismo a la comprensión del diseño como pueda realizar un arquitecto a través de renderizados, vídeos, etc. y que los diseños sean 17 visualmente agradables a la vista, es también un aspecto de interés, y puede ser un elemento diferenciador a la hora de participar en una licitación, por ejemplo, de urbanización en un entorno urbano. Por último, hay que mencionar que los programas generalmente utilizados en el mundo de la ingeniería de caminos, que permiten desarrollo de planos o modelos 3D son ampliamente de pago, como las herramientas desarrolladas por Autodesk como AutoCad o específicamente para la ingeniería de caminos en Civil 3D. Podrían resumirse, por tanto, los aspectos o requisitos positivos que un ingeniero de caminos, canales y puertos debe considerar en la elección de un programa de diseño en los siguientes: − Modelado 2D y 3D integrados en un mismo programa. − La capacidad de incorporar elementos propios de la ingeniería de caminos como puede ser un modelo del terreno a través de información geográfica. − La posibilidad de realizar elementos irregulares e imperfectos para representar elementos constructivos naturales como una roca. − Permitir realizar otras labores diferentes al propio diseño 3D de forma integrada o por compatibilidad directa con otros programas: cálculo de estructuras, cálculo hidráulico, mediciones, etc. − Compatibilidad con otros programas a través de la posibilidad de importar y exportar distintas fuentes de datos. − Permitir producir elementos visualmente atractivos, que sirvan para la captación de clientes y ayuden a una mejor comunicación y comprensión del proyecto para todas las partes como renderizados, vídeos o, por ejemplo, imágenes fotorrealistas. − Que el programa sea gratuito. − Capacidad de realizar modelos de grandes dimensiones sin un coste computacional elevado. − Poseer un gran número de usuarios y documentación de calidad, actualizada, gratuita y accesible, permitiéndose así el aprendizaje autodidacta del programa. Existen programas o conjuntos de programas que resuelven uno o más de estos aspectos, por ejemplo, para el diseño de una carretera, se puede por un lado utilizar Civil 3D y para su visualización 3D InfraWorks, pero el uso de varias herramientas sólo añade coste económico y complejidad, además del coste de aprendizaje de varias herramientas. Siguiendo con este mismo ejemplo, hay herramientas de trazado que permiten el desarrollo completo del mismo y su visualización 3D, como puede ser CLIP, sin embargo, la comunidad de este tipo de software es extremadamente reducida, su documentación es limitada y anticuada, por lo que suelen requerir de un coste económico añadido de asistencia técnica. Otro ejemplo podría ser el programa MIDAS que genera un modelo 3D de la estructura realizada y permite el cálculo estructural del mismo, sin embargo, este tipo de programas es muy especializado, y el diseño es muy limitado al estar orientado principalmente al cálculo de estructura. Una cuestión aun no mencionada, y de gran importancia en este documento y en general para el futuro de cualquier ingeniero, es la programación, y en concreto la capacidad del programa utilizado de permitir realizar cambios sobre el mismo, o de desarrollar complementos que permitan ampliar las capacidades del programa o automatizar procesos repetitivos. Esto puede suponer que un programa que no incluya uno de los aspectos anteriormente mencionados, mediante el desarrollo de complementos adquiera la capacidad cumpliendo así con una necesidad más del ingeniero. Existe la posibilidad de programar mediante programas comerciales de amplio uso como AutoCAD, mediante Autolisp, aunque la realidad es que es un pseudo-lenguaje muy específico, con poca comunidad y documentación para su aprendizaje; otro ejemplo podría ser el uso de Grasshopper 3D un «lenguaje de programación» visual con un editor basado en nodos desarrollado para su uso en la herramienta de diseño Rhinoceros 3D. Otra herramienta interesante en este aspecto es FreeCAD, que como su propio nombre indica es gratuita, y permite el desarrollo de funcionalidades a través de Python. Poseyendo este último ejemplo una gran ventaja en el ámbito de la programación con respecto a los otros ejemplos de programación mencionados, puesto que Python es uno de los lenguajes de programación más utilizados en la actualidad, y no ha sido desarrollado exprofeso para 18 un programa de diseño, lo que permite acceder a un gran número de librerías o su propio desarrollo sin limitaciones de ningún tipo. 1.2. Sobre Blender De la misma forma que la ingeniería de caminos ha adoptado de forma tardía ciertas tecnologías del mundo de la informática frente a otras ingenierías. Las ingenierías, en general, adoptan estas tecnologías con retraso frente a otros sectores, por ejemplo, el denominado Modelado de Información de Construcción (la primera herramienta BIM fue desarrollada en 1984), no es más que la incorporación de almacenamiento de datos en un elemento, el análisis de los mismos y su distribución, aspectos que cualquier persona con conocimientos de programación puede entender como básicos. De hecho, ya la ingeniería de caminos lleva utilizando una tecnología con algunas de estas cualidades desde hace mucho tiempo como son las herramientas SIG (la primera herramienta SIG fue desarrollada en 1962). Esto no deja de ser cierto en el diseño 3D, y en este caso probablemente por las inversiones que otros sectores realizan en programas dedicados al diseño 3D como el mundo de los videojuegos, la animación y la cinematografía, que añaden una serie de requisitos que a priori no son de importancia para un ingeniero, pero que permiten el desarrollo de diseños: fotorrealistas, extremadamente complejos, con la posibilidad de interactuar con los modelos realizados, etc. Un programa de este estilo es Blender, que como en su propia página describe es un programa gratuito y de código abierto para el diseño 3D que «soporta el proceso completo del desarrollo 3D, modelado, rigging, animación, simulación, renderizado, composición y captura de movimientos e incluso la edición de vídeos», todo ello, en un entorno totalmente modular y programable mediante Python, con la capacidad de utilizar y desarrollar complementos (add-ons) tanto gratuitos como de pago. Permite también el esculpido, el dibujo 2D, la composición de texturas, generación de uv maps, manejo de iluminación, realizar simulaciones físicas (con gravedad) de fluidos, humo, partículas, etc…y de manera gratuita. Blender soporta una amplia variedad de formatos, que incluyen los formatos generalmente más utilizados. Siendo por tanto compatible con otros programas, y permitiendo así que los resultados puedan ser distribuidos para su uso en otros programas. Así, se incluyen entre otros formatos, para las imágenes los formatos: .bmp, .png, .jpg, .tiff, etc.; para los vídeos formatos: .mp4, .avi, etc.; como formatos para el diseño tridimensionales: .dxf, .3ds, etc.; y para el diseño bidimensional: .svg, .gimp, etc. Siendo además posible desarrollar códigos que añadan la compatibilidad con nuevos formatos no soportados de forma directa o incluso utilizar complementos públicos desarrollados por otros usuarios para este fin. Blender tiene además una comunidad de usuarios y documentación extraordinaria que permite el aprendizaje y resolución de dudas sin coste económico alguno a través de internet. Por todo ello, Blender, aun sin ser un programa desarrollado para la ingeniería, parece ser una herramienta bastante interesante que aparentemente puede cumplir con los aspectos de interés anteriormente definidos para un ingeniero. 1.3. Objetivos El objetivo de este trabajo es estudiar el uso de Blender como herramienta para la ingeniería de caminos canales y puertos, tratando de demostrar que cumple con los requisitos anteriormente definidos y que podrían ser de utilidad para el ingeniero en el desarrollo de sus proyectos. Como ya se ha mencionado el aspecto de la gratuidad ya está cumplido al ser Blender una herramienta distribuida bajo licencia GNU GPL. Según se indica en la propia página de Blender «Get the world’s best 3D CG technology in the hands of artists as free/open source software» [1]. Al ser Blender una herramienta que no ha sido desarrollada exprofeso para la ingeniería de caminos, canales y puertos, es comprensible que no posea de manera directa herramientas específicas para esta profesión, por lo que a lo largo de este documento se comprobará la utilidad del programa a través del desarrollo de 19 herramientas que podrían ser de aplicación para esta rama de la ingeniería. Hay que aclarar que este documento no pretende ser un curso del uso de Blender a través de su interfaz gráfica, de diseño o de programación, sino la demostración de la viabilidad de uso de esta herramienta para la ingeniería de caminos, canales y puertos a través de aplicaciones de interés para un ingeniero de caminos. En concreto, en el documento se busca demostrar la viabilidad de uso del programa para la ingeniería de caminos a través del desarrollo de las siguientes aplicaciones: − Caminos: realizar modelos tridimensionales del terreno y extraer perfiles longitudinales de carreteras. − Canales: crear tuberías normalizadas para distintos materiales. − Puertos: crear rocas naturales y bloques de hormigón artificial para diques. 20 2. Blender 2.1. Introducción Blender es una herramienta gratuita desarrollada para el diseño 3D, dirigida principalmente al sector de las animaciones y los videojuegos. El modelado se realiza a través de los siguientes tipos de objeto: − Mallas: superficies definidas mediante vértices. − Curvas: polilíneas, Bézier y NURBS, que pueden ser 2D o 3D. − Superficies: definidos mediante puntos de control como las curvas tipo NURBS. − Metaball: superficies definidas mediante fórmulas matemáticas, que son capaces de interactuar entre sí cuando están cerca. − Texto. − Volúmenes: elementos utilizados para representar objetos volumétricos dispersos OpenVDB, como una nube, agua, una explosión, etc. − Elementos vacíos. − Enrejados. − Imágenes. − Amatures: permiten generar o restringir movimientos en los objetos. − Luces. − Cámaras. − Campos de fuerza. Los tipos de objetos de utilidad directa para la ingeniería son las mallas y curvas, con las que se realiza el modelado de forma diferente a las herramientas CAD tradicionales, a lo largo del documento se analizará el funcionamiento de estas dos tipologías. Algunas características de Blender son: − Permite la programación a través de Python y la creación y uso de complementos (add-ons). − Incluye herramientas de audio, vídeo y tratamiento y creación de imágenes y texturas. − Para el renderizado posee varios motores gráficos: Evee, WorkBench y Cycles. − Permite realizar simulaciones físicas, con sólidos rígidos, sólidos deformables, fluidos y humo, permitiendo introducir conceptos físicos como la gravedad. − Es totalmente modular, pudiéndose cambiar la práctica totalidad del entorno de trabajo, personalizándolo a las necesidades y gustos del usuario. Por lo general el uso de la interfaz gráfica se lleva a cabo mediante atajos de teclado, esto fomenta el uso rápido del programa con el tiempo, sin embargo, esto mismo complica el aprendizaje. En este sentido es importante mencionar que el programa no es sencillo de utilizar, debido a la gran cantidad y variedad de herramientas que posee y a la tipología de objetos utilizada en el diseño distinta de las líneas, sombreados y sólidos utilizados en otros programas de diseño para la ingeniería que son más intuitivos de usar. Por todo esto, la curva de aprendizaje del programa es larga, si bien con el tiempo y gracias al fomento del uso de los atajos, se puede adquirir una gran velocidad en el modelado. Además de los atajos, los menús y botones 21 que contiene el programa, es posible acceder a las herramientas mediante una línea de comandos similar a la existente en programas como Autocad. 2.2. Entorno de trabajo El entorno de trabajo de Blender se compone de 3 partes principales, el menú, los diseños (layouts) y las ventanas de edición. Además, cada ventana, de entre todas las disponibles, cuenta con sus propias herramientas, menús deslizables, paneles emergentes, botones, etc, ver Figura 1. Las ventanas incluyen también distintos modos de trabajo que cambian casi por completo el funcionamiento y las utilidades de la misma, esto se puede ver por ejemplo con la ventana 3D Viewport, que es una de las ventanas de edición de los modelos y que contiene los modos: Object Mode, Edit Mode, Sculpt mode, Vertex Paint, Weight Paint y Texture Paint. También se incluyen en la ventana 3D Viewport, que puede ser considerada como la ventana principal de Blender, la posibilidad de decidir cómo se desea ver el entorno: vista en perspectiva u ortogonal; visualización de los objetos en modo: alambre, sólido, vista previa y renderizado; y múltiples opciones más. Figura 1. Layout utilizado en Blender, se aprecian en el 6 de las 21 ventanas existentes, así como algunas herramientas. A continuación, se incluye un listado las ventanas de edición existentes en la versión 2.92 de Blender utilizada en el desarrollo de este trabajo: − De tipo general: ▪ 3D Viewport: es la ventana utilizada para la edición del modelo siendo usada para múltiples propósitos como el modelado, el pintado o la animación entre otros. 22 Figura 2. Ventana de edición 3D Viewport en modo Edit mode. ▪ Image Editor: utilizada para la edición de elementos tipo imagen. ▪ UV Editor: se utiliza para la edición de uv maps. ▪ Shader Editor: utilizada para la edición de materiales que dan color a los elementos. Realizando la edición a través de un «lenguaje de programación» visual basado en nodos y uniones. ▪ Compositor: para la composición de imágenes o películas mediante un sistema de nodos y uniones. ▪ Geometry Node Editor: para la edición de geometrías mediante nodos y uniones en el modificador (modifier) Geometry Node. ▪ Texture Nodes: para la generación de texturas mediante un sistema de nodos y uniones. ▪ Video Sequencer: utilizada para la edición de vídeos. ▪ Movie Clip Editor. − Para la animación: ▪ Dope Sheet. ▪ Timeline. ▪ Graph Editor. ▪ Drivers Editor. ▪ Nonlinear Animation. − Para la programación: ▪ Text Editor: editor de texto para la programación y ejecución de códigos. ▪ Python Console: consola de Python que permite el acceso directo a la API de Blender [2]. ▪ Info Editor: es un historial de las acciones que se han realizado en el programa, así como de los mensajes de advertencia y error que puedan surgir. − Para el manejo de datos: ▪ Outliner: ventana que permite visualizar los elementos del modelo agrupados en conjuntos llamados escenas y colecciones, en un sistema de carpetas en cascada arborescente. Permite activar o desactivar la visualización de los elementos del modelo, el cambio de nombre su selección o eliminación entre otros. 23 ▪ Properties: utilizada para la edición de variables de la escena o el objeto activo. Posee múltiples categorías, que son cada uno de los iconos que se ven en la parte izquierda de la siguiente figura, y que incluyen infinidad de parámetros y herramientas. Figura 3. Ventana de edición Properties. ▪ File Browser: buscador de archivo similar al explorador de Windows. ▪ Preferences: permite cambiar las preferencias de Blender, esta misma ventana de edición es accesible a través del menú principal del programa. Debe mencionarse que Blender permite una gran personalización del entorno de trabajo, desde los colores utilizados en él mismo, hasta la elección de los atajos a utilizar para cada acción que en él se realiza. Además de esto, los distintos layouts, pueden ser modificados para cubrir las necesidades del usuario, e incluso se pueden generar nuevos layouts y guardarlos. Por último, hay que mencionar que Blender incluye algunas funcionalidades del tipo snap on point, que permiten el encaje por vértices, aristas o caras, así como otros elementos del espacio de modelo. Aunque la utilidad es bastante limitada entre otras cuestiones porque no permite realizar traslaciones, escalados o rotaciones a la vez se realizan movimientos del espacio modelo, como si permiten otras herramientas de diseño. Sin embargo, y para que sirva de introducción a las capacidades de personalización infinitas que tiene el programa, existe un complemento gratuito denominado CAD Transform que permite realizar transformaciones en los objetos a la vez que se realizan movimientos en el espacio modelo, incluyendo éste un número de utilidades de tipo snap on point que para hacerse una idea de las posibilidades que permite posee 31 atajos, que se pueden además concatenar para obtener resultados distintos. Permite además dar marcha atrás en la combinación de atajos realizada sin perderla por completo, y utilizar variedad de unidades y sistemas de medida, así como operaciones matemáticas. Todo esto hace que algo que a priori era un problema para el mundo de la ingeniería por la falta de precisión que la ausencia de capacidades de tipo snap on point produce a la hora de realizar el diseño, con la incorporación de un complemento, se convierta en un entorno de trabajo con más capacidades en este sentido que cualquier otro programa de diseño. Además, al ser el complemento gratuito y de código libre, permite al usuario realizar cambios en el código y añadir nuevas funcionalidades si es necesario. 24 2.3. Python Como se ha mencionado Blender, permite la interacción mediante consola y ejecución de códigos mediante el editor de textos incorporado a través de lenguaje de programación Python. Además, es posible la creación de complementos para integrar herramientas en los distintos entornos de trabajo que posee el programa. Para facilitar, el desarrollo contiene herramientas dedicadas al mismo como son las ya mencionadas ventanas de edición Text Editor, Python Console y Info Editor. Permite visualizar las líneas de código que se ejecutan al usar una herramienta colocando el ratón sobre la misma si se tienen activadas las opciones dedicadas al desarrollo que se encuentran en las preferencias del menú edición. Además, permite copiar dichas líneas para su uso pulsando con el botón derecho del ratón. Se puede acceder al código fuente de la herramienta si es de código libre pulsando con el botón derecho del ratón. También se puede acceder a plantillas de código para los distintos tipos de complementos a través del editor de texto. Por último, incluye la capacidad de visualizar los índices de los vértices, aristas y caras de las mallas, su orientación, longitud área, etc. Todo esto facilita enormemente el desarrollo de herramientas en el programa. Es muy importante en cuanto al aprendizaje de un programa que este posea una comunidad grande y activa, así como una documentación accesible, actualizada y completa. Esto facilita el aprendizaje al permitir realizar consultas rápidas en la documentación, o mediante foros o vídeos. Esta cuestión es de gran importancia en el mundo de la programación, teniendo Blender una comunidad lo suficientemente grande como para resolver la mayoría de las dudas que puedan surgir en el aprendizaje a través de internet, aspecto muy positivo a la hora de escoger un programa para trabajar. Figura 4. Editor de texto incluido en Blender con una plantilla para realizar complementos con los que crear objetos. 25 2.4.Herramientas desarrolladas A lo largo del documento se desarrollan cinco herramientas relacionadas con la ingeniería de caminos, canales y puertos, que, en concreto están ordenadas por su relación con el ámbito de los caminos, los canales o los puertos, como guiño hacia el nombre de la profesión. Cada problema en cierta medida pretende demostrar la capacidad que un ingeniero a través de Blender tiene para cumplir con alguno de los requisitos definidos en el primer apartado y que pueden ser de utilidad para el desarrollo de la profesión, y demostrar así el cumplimiento de los objetivos de este trabajo. Para cada herramienta se realizará una exposición del desarrollo que hay que realizar para la resolución del problema pertinente, y una breve reseña de los resultados obtenidos mediante la misma. Para no complicar de forma excesiva el documento y facilitar su lectura a cualquier tipo de ingeniero sean cuales sean sus conocimientos de programación, se tratará en la medida de lo posible de no complicar en exceso la explicación en este aspecto. Motivo por el cual los códigos adjuntos en el correspondiente anexo no deben ser tomados como modelo a seguir en la programación. Es por ello por lo que se ha optado por no desarrollar complementos capaces de ser distribuidos para su uso mediante instalación, que complicarían y alargarían en exceso este documento. Si bien, en una de las herramientas desarrolladas sí que se explicará el desarrollo e instalación de un tipo de complemento de los muchos posibles en Blender, para así completar el documento, con el desarrollo completo de una idea que pretende resolver un problema ingenieril. Es posible tomar los códigos desarrollados y ejecutarlos para replicar los resultados sin ningún problema o realizar los cambios pertinentes en los mismos para el desarrollo de nuevos complementos con estos. Su uso o instalación en el caso del complemento desarrollado se explica al final de cada apartado. Por último, hay que mencionar que los códigos han sido desarrollados y comprobados en la versión 2.92 de Blender y que no se ha implementado ninguna medida de comprobación de compatibilidad de los módulos utilizados o de Blender, ni de validez de los archivos utilizados, con el fin mencionado anteriormente de no complicar en exceso los códigos con, en este caso, cuestiones que no forman parte del objetivo de este documento. 32 nombre_del_modificador, type='SUBSURF') # Se crea el modificador tipo # Subdivision Surface para # el objeto. subivision_surface_mod.subdivision_type = 'SIMPLE' # Tipo de subdivisión subivision_surface_mod.levels = 6 # Número de divisiones a # realizar. # 3D Viewport. subivision_surface_mod.render_levels = 6 # Número de divisiones a # realizar para el # renderizado. displace_modifier = obj.modifiers.new( nombre_del_modificador, type='DISPLACE') # Se crea el modificador tipo # Displace para el objeto. displace_modifier.texture = textura # Se añade la textura cuyos # datos serán tomados para # producir los # desplazamientos de los # vértices. mediante los atributos levels y render_levels, se controla la precisión del modelo en el desarrollo y su renderizado y por tanto el consumo que se hace de los recursos del ordenador. Resultados y uso de la herramienta creada Para el uso de esta herramienta, se toma el código terrain_creator como en el apartado anterior y se introduce en una ventana tipo Text Editor añadiendo al final del mismo en este caso las líneas siguientes: tiff_dem = Tiff( filepath=ruta_al_archivo, mode='MODIFIER', name=nombre_del_dem_resultante) tiff_dem.blendify() como se puede ver el único cambio realizado respecto al modelo anterior es el argumento mode, que anteriormente estaba establecido por defecto con valor 'DELAUNAY'. El resultado obtenido puede verse en las siguientes figuras para varios niveles de división sobre la superficie. Se puede comprobar que en este caso las caras de la malla no son triángulos, dado que la división realizada por el modificador consiste en una división por la mitad de cada arista. 33 Figura 10. Modelo tridimensional del terreno generado a partir de modificadores con Blender. a) 3 divisiones, b) 6 divisiones, c) 9 divisiones y d) 11 divisiones. 3.1.3. Añadir materiales al modelo del terreno Desarrollo A diferencia de la herramienta anterior el objetivo de la textura generada en este caso (material) si es el de dar color a la malla. En este apartado, se verá cómo crear un material que aporte color a través de una rampa de colores en función de altimetría, y otro material que aporte color mediante el uso de una ortofoto, que puede ser obtenida, como la altimetría, a través del centro de descargas del IGN en España. Para tener un pequeño descanso de la programación y utilizar un poco la interfaz gráfica de Blender el primera material se definirá de forma manual en la ventana denominada Shader Editor y se explicará cómo se programa con el segunda material ya teniendo una noción del funcionamiento de la definición de materiales complejos con Blender. Si bien ambos materiales han sido programados y se encuentra en el correspondiente código (ver 257 para el material tipo rampa de colores y 373 para el material tipo ortofoto en el anexo I). Rampa de colores La edición manual de materiales se realiza a través de la ventana Shader Editor en la que se definen los materiales con nodos (nodes) y uniones de estos (links), como un «lenguaje de programación» visual. Los nodos por su parte poseen unos atributos que permiten realizar cambios sobre el material, y que dependerán del tipo de nodo utilizado, así como unas entradas (inputs) y salidas (outputs), que son los datos requeridos por el nodo, y los resultantes del mismo, de manera que existen también nodos de entrada de datos y un nodo final denominado (Material Output Node) que es el material resultante. Es el conjunto de nodos (node tree) el que genera el resultado final al ir realizando cambios sobre los datos de entrada en los nodos intermedios. Es posible también crear nodos a partir de un conjunto de nodos (node group). Para definir un material lo primero que debe hacerse es crear el material (pulsar el botón New), donde aparecerá el material base que incluye los dos nodos más utilizados, que son el nodo final Material Output y el nodo Principled BSDF que eliminaremos en este caso. Para poder realizar una rampa de colores en el modelo como las que se pueden generar en las herramientas SIG sobre las capas ráster, es necesario en primer lugar definir la propia rampa que de forma general se define como la división de un intervalo unidad mediante distintos colores, e interpolando la zonas entre colores. Los colores en este caso deben ser establecidos en función de la altimetría, por lo que es necesario 34 extraer los límites máximo y mínimo del modelo, y posteriormente normalizarlos al rango unidad con el que se define la rampa de colores. Por último, sólo es necesario aportar la geometría del conjunto para saber a qué lugar corresponde cada color. Para añadir un nodo se busca en el menú Add de la ventana Shader Editor y se selecciona para que aparezca en el editor. Cambiar las propiedades de cada nodo es tan sencillo como escribir un número, mover el cursor habiendo pinchado sobre la misma, etc. es decir, un manejo igual al de cualquier otra ventana gráfica. Para unir nodos se pincha con el cursor sobre uno de los círculos del lateral derecho de un nodo (output) y se arrastra este hacia uno de los círculos del lateral izquierdo (input) de otro nodo, momento en el que se deja de pulsar el ratón, quedando la conexión fijada. Para saber qué elementos de entrada y salida conectar la interfaz ayuda a través de un código de colores y el nombre de cada elemento. De esta forma se añaden los siguientes nodos: − Geometry: Otorga la información geométrica de la malla al material. − Separate XYZ: Separa los valores de la geometría a valores individuales X, Y y Z; del que el material en este caso sólo necesita los valores Z. − Value: Para representar la Z mínima para realizar la normalización al rango unitario de la rampa de colores. − Value: Para representar la Z máxima para realizar la normalización al rango unitario de la rampa de colores. − ColorRamp: Representa la rampa de colores. Con los botones + y - se añaden colores a la rampa, y pinchando en cada color o marcando su índice se permite el cambio de color. En el cuadro de texto Pos (Position) se indica la posición del color en el rango unitario. Y se conectan entre sí como aparece en la siguiente figura: Figura 11. Material tipo rampa de colores para un modelo tridimensional del terreno. En la figura aparece otro nodo denominado Normalize que como su propio nombre indica realiza la normalización de la altimetría, pero este nodo no es un nodo como tal sino un node group que hay que crear. Para ello, se añaden los siguientes nodos: − Math: seleccionándolo como función Subtract y tomando los valores de la geometría y el valor mínimo para hacer la diferencia entre ellos de manera que queden valores que empiecen en cero. − Math: seleccionándolo como función Subtract y tomando los valores máximo y mínimo cuya diferencia se usa para conocer el tamaño del rango de valores original. − Math: seleccionándolo como función Divide para realizar la normalización con los valores anteriores. Se seleccionan y se agrupan con Add → Group → Make Group, apareciendo los nodos node input y node output se sale de la ventana de edición del grupo de nodos mediante el botón con un icono de flecha hacia arriba que se encuentra en la esquina superior derecha de la ventana Shader Editor y aparece en la ventana el nuevo nodo creado. 35 Figura 12. Conjunto de nodos para la normalización. Por último, si este material no había sido creado con la malla seleccionada se añade el material a la malla cambiando al modo de edición al cambiar de Object Mode a Edit mode en la ventana 3D Viewport y pulsando ahora en la ventana Properties en el apartado de Material Properties en assign. Finalmente, para poder ver los colores hay que estar en el modo Solid o Rendered del 3D Viewport, lo cual se cambia en los botones con iconos en forma de circunferencia que hay en la esquina superior derecha del 3D Viewport. Ortofoto Conociendo el funcionamiento de la edición de nodos a partir del desarrollo manual anterior, el desarrollo a través de la API de Blender se entiende con mayor facilidad. En primer lugar, se crea un material, y se define el árbol de nodos (node tree) como un conjunto de nodos unidos entre sí. Para ello se crean los nodos en el material y se unen los mismos en función del parámetro input y output requerido. En este caso son necesarias los siguientes nodos: − Image Texture: utiliza la imagen como dato de entrada para establecer el color. Por lo que es necesario añadir una imagen en Blender como se hizo en el apartado de los modificadores, pero esta vez con atributo colorspace_settings.name en modo sRGB. − Principled BSDF: contiene las propiedades más utilizadas en la creación de un material en un mismo nodo. − Material Output: nodo final de cada material. las líneas tipo quedarían de la siguiente forma: material = bpy.data.materials.new( name=nombre_del_material) # Se crea el material # con el que de forma # automática se # generan los nodos # Principled BSDF y # Material Output. material_nodes.new('ShaderNodeTexImage') # Se crea el nodo # Image Texture que # servirá como input # para la imagen. material_nodes['Image Texture'].image = imagen # Se especifica la # imagen a utilizar en 36 # el nodo Image # Texture. De la # misma forma se # definen los # distintos # propiedades de cada # nodo. material.node_tree.links.new( material_nodes['Image Texture'].outputs['Color'], material_nodes['Principled BSDF'].inputs['Base Color']) # Se unen los nodos # Image Texture y # Principled BSDF # mediante los # correspondientes # elementos. material.node_tree.links.new( material_nodes['Principled BSDF'].outputs['BSDF'], material_nodes['Material Output'].inputs['Surface']) # Se unen los nodos # Principled BSDF y # Material Output # mediante los # correspondientes # elementos. objeto.data.materials.append(material) # Se aplica el # material sobre el # objeto activo. Figura 13. Material tipo ortofoto. 37 Resultados y uso de la herramienta creada A continuación, se muestran los resultados de la asignación de los materiales definidos sobre los modelos del terreno. Hay que recordar que sólo serán visibles si la ventana 3D Viewport se encuentra en modo Solid o en modo Rendered. Para utilizar, el código desarrollado, terrain_creator, se utilizan en primer lugar las mismas líneas definidas en el apartado anterior para generar el modelo del terreno, y a estas se les aplica uno de los siguientes métodos: tiff_dem.set_rc_material( color_ramp=nombre_de_la_rampa_de_colores) # Para utilizar una rampa de colores. # Se han creado dos rampas con # nombres RGB y ELEVATION. tiff_dem.set_o_material( filepath=ruta_a_la_ortofoto) # Para utilizar una ortofoto. Figura 14. Modelo tridimensional del terreno coloreado mediante una rampa de colores definida en función de la altura. 38 Para demostrar las capacidades de Blender, la siguiente figura ha sido generada con una malla de 4,2 millones de caras y la ortofoto [6] utilizada es de 0,75 gigabytes. Figura 15. Modelo tridimensional del terreno coloreado mediante una ortofoto. Se puede ver en estas últimas figuras, que la malla no es apreciable, esto no se debe a la precisión de la mismas sino a que se ha añadido un proceso de suavizado (Smoothing) sobre las mallas, otra propiedad interesante que incluye Blender y que ayuda a un resultado más realista y vistoso de los modelos. En la siguiente figura se puede apreciar mejor el cambio entre una malla no suavizada y una suavizada: Figura 16. Comparativa de malla suavizada a) y no suavizada b). 3.1.4. Blender GIS Si bien en los apartados anteriores se ha desarrollado la metodología y el código necesarios para producir un modelo tridimensional del terreno, esta idea no es nueva para Blender, y es que las industrias que dan uso a este programa tienen también la necesidad de generar modelos tipo terreno, para conformar los ambientes en los que se desarrollan los videojuegos, animaciones o películas. Es por esto por lo que métodos 39 similares a los que se han descrito, pero por lo general sin la necesidad de partir de un dato real (la altimetría usada), sino que, mediante la generación de elevaciones por ruido, son utilizados de forma habitual en Blender. En concreto el complemento gratuito Blender GIS permite realizar modelos tridimensionales del terreno a partir de imágenes como se ha realizado anteriormente, pero también mediante curvas de nivel (.shp). Permite la descarga directa de datos espaciales como la altimetría, ortofotos, edificios, carreteras, etc. de servicios web como Open Street Map. Igualando de esta forma el servicio ofrecido por Infraworks, pero permitiendo en todo momento el control total del modelo por parte del usuario, así puede utilizarse la ortofoto procedente de un servicio web, los edificios, carreteras, etc. y a la vez utilizar una fuente propia para el modelo de elevaciones que está limitado por los servicios web a 30x30m de precisión. En la siguiente figura se muestra un modelo obtenido a través de Blender GIS, que contiene una ortofoto, las elevaciones del terreno y edificios, todo ello extraído de servicios web incluidos en la herramienta. Figura 17. Modelo generado con Blender GIS con ortofoto, altimetría y edificios (Nueva York). Conclusiones Como se ha podido ver la programación en Blender es relativamente sencilla, e incluso permite realizar programas con resultados increíbles sin apenas conocimientos de la API, además, incorpora una serie de herramientas, los modifiers, que simplifican una gran variedad de procesos de gran utilidad en el desarrollo de aplicaciones puesto que de lo contrario tendrían que ser desarrollados por el programador. Además, con el uso de archivos de distintos formatos para la obtención del modelo de elevaciones tanto si son directamente soportados por Blender o alguna librería de Python, como si no lo son, se ha demostrado la compatibilidad del programa con distintas fuentes de información. También se ha comprobado en este caso que no existe limite en la precisión del modelo digital a generar más que el impuesto por los datos utilizados y la capacidad de ordenador que se tenga, ayudando Blender a realizar modelos complejos y de gran tamaño con poca capacidad computacional a través, como se ha visto, de distintos aspectos en el modelado, evitando así que el programa se quede bloqueado de forma constante o vaya lento como puede ocurrir con otros programas de diseño. 40 3.2. Extracción de perfiles longitudinales de una carretera Continuando con la temática del terreno en el ámbito de las carreteras en este apartado se desarrolla una herramienta que permite extraer perfiles longitudinales para carreteras a través un perfil en planta conocido. Con esto se demuestra la capacidad que tiene el programa de realizar elementos bidimensionales. Esto es indispensable al ser los planos parte fundamental en la documentación actual de los proyectos de ingeniería de caminos, canales y puertos. Para la definición de elementos bidimensionales es posible utilizar mallas planas (por defecto Blender incluye el plano como elemento primitivo). Sin embargo, son las curvas las que toman mayor protagonismo en este ámbito. En concreto el programa tiene capacidad de realizar tres tipos principales de curvas. En primer lugar, las curvas tipo NURBS, definidas a través de un orden, y un conjunto de puntos de control a los que se les asigna un peso, de manera que cada punto de la curva se calcula como una suma ponderada de los (orden) puntos más cercanos. En segundo lugar, las curvas tipo Bézier, en las que se unen dos puntos de anclaje a través de una curva definida por puntos de control mediante funciones polinómicas. Y, por último, las polilíneas. Desarrollo Para la creación del perfil longitudinal de una carretera existente es necesario como información de partida: − La altimetría de la zona, cuyas fuentes pueden ser las mismas que se han utilizado en el apartado anterior. En este caso se ha optado por un archivo .tiff. − El trazado en planta de la carretera. Este se puede definir directamente en Blender con el uso de una ortofoto o mediante la información geográfica existente en centros de descarga públicos como el centro de descargas del IGN en España, a través de una capa tipo Shape de carreteras. En este caso se ha optado por una capa Shape de carreteras procedente del IGN. La información de la altimetría es accesible mediante las técnicas ya desarrolladas en el apartado anterior. Mientras que la información procedente del archivo Shape puede ser obtenida mediante la creación de un lector propio como se hizo con los archivos .asc o mediante librerías externas como se hizo con los archivos .tiff. Sin embargo, por simplificar, y aunque los archivos Shape no son de gran complejidad, en este apartado se optado por extraer la información de los vértices que componen la polilínea de una única carretera en un archivo .csv mediante QGIS que tiene la misma estructura que tenía el archivo .xyz descrito en el apartado anterior. Con la información de la altimetría y la posición de los vértices que definen el trazado en planta de la carretera, únicamente es necesario extraer el valor de la cota perteneciente a cada vértice de la altimetría. Es posible, si se considera necesario, incrementar el número de vértices definiendo la posición de nuevos vértices mediante geometría básica, al extraer nuevos vértices de las rectas que unen dos vértices conocidos. Para obtener la cota de cada vértice únicamente hay que definir una transformación del sistema de coordenadas de los mismos al sistema de coordenadas propio de la información altimétrica, que para este caso es el propio de una imagen georreferenciada (una matriz FxC georreferenciada). Conociendo toda la información de los vértices del trazado en planta, se debe calcular la distancia proyectada sobre un plano horizontal existente entre un vértice y el origen del perfil (vértice inicial). Esto se obtiene con el módulo del vector que une los dos vértices. Así el perfil longitudinal quedaría definido por las cotas de los vértices y las distancias. Para modelar todo esto se ha creado las siguientes clases: − Una que representa cada vértice del perfil denominada Point2D. − Otra que representa un vector bidimensional definido para el cálculo de las distancias entre puntos denominada Vector2D. − Y otra que representa al perfil longitudinal y que lee los archivos de altimetría y trazado en planta, obteniendo la información de altura y distancia requerida denominada Profile. Por otro lado, para poder crear una gráfica con sus ejes, etiquetas, marcas, etc. que contenga la información 41 necesaria del perfil longitudinal, se crean también las siguientes clases: − Para definir las propiedades de cada eje (escala, separación de marcas, etc.) se crea la clase TickProperty. − Para utilizar toda la información creada y dibujar el perfil en Blender, se crea la clase ProfilePloter, que recibe como atributos el perfil creado y las propiedades de cada eje, así como el valor que se desea para el primer punto kilométrico o las escalas a utilizar para cada eje. La clase ProfilePloter crea mediante el método blendify un conjunto de objetos que componen la representación gráfica del perfil longitudinal. Estos objetos son los siguientes: − Polilíneas para representar el marco de la gráfica y las marcas sobre la esta. Para crear este tipo de curva en Blender, en primer lugar, es necesario crear el elemento “curva”; este tipo de elemento está conformado a su vez de un conjunto de splines, que se definen mediante las coordenadas de sus vértices. La creación tipo de una curva de dos dimensiones tipo polilínea quedaría de la siguiente forma: curva = bpy.data.curves.new( name=nombre_de_la_curva, type='CURVE') # Se crea la curva curva.dimensions = '2D' # Se indica que la curva será # bidimensional. cur_spline = curva.splines.new('POLY') # Se crea un spline de tipo # polilínea. cur_spline.points.add(1) # Se añaden n vértices al spline. cur_spline.points[n].co = ( coordenada_X, coordenada_Y, 0.0) # Se indican las coordenadas # (distancia al origen y cota) # del vértice. obj = bpy.data.objects.new('X Ticks', curva) # Se crea el objeto. − Curvas, en este caso se ha escogido las de tipo Bézier para la representación gráfica del perfil longitudinal. Para crear este tipo de curvas el procedimiento es similar al definido con las polilíneas, si bien es necesario en este caso definir también los puntos de control de cada vértice. En este caso se han utilizado puntos de control de tipo AUTO (automáticos) para simplificar el proceso, obteniéndose buenos resultados. La creación tipo de una curva de dos dimensiones tipo Bézier quedaría de la siguiente forma: curva = bpy.data.curves.new( name=nombre_de_la_curva, type='CURVE') # Se crea la curva. curva.dimensions = '2D' # Se indica que la curva # será bidimensional. cur_spline = curva.splines.new('BEZIER') # Se crea un spline de # tipo Bézier. cur_spline.bezier_points.add(n) # Se añaden n vértices # al spline. cur_spline.bezier_points[0].co( coordenada_X, coordenada_Y, 0.0) # Se indican las # coordenadas (distancia # al origen y cota) del # los vértices. cur_spline.bezier_points[n].handle_right_type = 'AUTO'# Se indica el tipo de # punto de control # izquierdo. cur_spline.bezier_points[n].handle_left_type = 'AUTO' # Se indica el tipo de 48 # a la nueva geometría, cambiando la malla creada, # mediante el código que genera la geometría del # prisma tubular. def register(): # Esta función sólo se ejecuta para activar el # código creado, siendo la función a la que se llama # desde el archivo __init__ al activar el # complemento. bpy.utils.register_class(OBJECT_OT_pipe) bpy.types.VIEW3D_MT_mesh_add.append(menu_func) def unregister(): # Esta función sólo se ejecuta al desactivar el # código, para desactivar todos los cambios # realizados por la función register. Siendo por # tanto la función llamada al desactivar el # complemento desde el archivo __init__. bpy.utils.unregister_class(OBJECT_OT_pipe) bpy.types.VIEW3D_MT_mesh_add.remove Por tanto, en la función execute lo que se hace es ejecutar el código que genera la geometría incluyéndose las distintas formas en la que esto se lleva a cabo mediante condicionales. Y en la función draw se diseña el panel del complemento creado. Para ello se utilizan entre otras las siguientes líneas: estructura = self.layout # Objeto con el que se define la # estructura del panel del complemento. columna = estructura.column() # Crea una columna en el panel. columna.prop(self, nombre_del_atributo) # Añade un atributo a la columna. Debe mencionarse que cada cambio dentro del panel puede producir cambios además de en el propio objeto, en el panel, por lo que esta función también es ejecutada con cada cambio realizado en los atributos. Resultados, instalación y uso del complemento creado El complemento desarrollado (965 y 990 en anexo II), es algo más complejo de lo que se ha descrito, he incluye la capacidad de escoger si se desea realizar la tubería mediante datos de tuberías normalizadas o de forma libre. En función de la elección, que se realiza a través de una casilla, la interfaz permite una forma u otra de elección de los diámetros para lo cual cambia su diseño. Además, se incluye la capacidad de realizar transformaciones (traslaciones y rotaciones), en la pieza desde el panel. Se incluye también la generación del material (texturas) correspondiente al material de tubería elegido. Por último, también en función de los valores de ciertos atributos que se hayan escogido se muestran o no en el panel las medidas, en concreto si se escoge la creación de tuberías normalizadas, se muestran los valores correspondientes a las medidas normalizadas. En la siguiente figura se pueden ver las distintas formas que toma el panel gráfico en función de los atributos elegidos. 49 Figura 22. Diseños que adopta el panel del complemento creado en función de los parámetros escogidos: a) tubería no normalizada, b) tubería normalizada de HA, c) tubería normalizada de polietileno y d) tubería normalizada de PVC. 50 Para poder utilizar el complemento sin instalarlo, se puede ejecutar el código add_pipe desde el editor de texto llamando a la función register del archivo que contiene el código del complemento, y añadiendo el inicio de este la variable bl_info que se incluyó dentro del archivo __init__. Si se quiere instalar, primero se crea un .zip con el archivo __init__ y el archivo add_pipe. Posteriormente se instala buscando el .zip en el apartado de add-ons de las preferencias del menú de edición pinchando en el botón Install... Figura 23. Tuberías obtenidas a través del complemento creado: a) de hormigón armado DN 400, b) de función dúctil DN 500, c) de polietileno de alta densidad PN-16 DN 315 y d) de PVC PN-16 DN 250. Figura 24. Tuberías obtenidas a través del complemento creado con los materiales elegidos para las mismas: a) de hormigón armado DN 400, b) de función dúctil DN 500, c) de polietileno de alta densidad PN-16 DN 315 y d) de PVC PN-16 DN 250. 51 Como se puede ver el resultado es fotorrealista. Y esto se ha conseguido sin entrar en el tema de la iluminación, cámaras y renderizado de Blender, y sin realizar grandes esfuerzos en la creación del material, ya que ello complicaría en exceso el trabajo, pudiendo ser por tanto los resultados mucho mejores y realistas que los obtenidos. Conclusiones Se ha logrado en este apartado ver la capacidad de integrar herramientas propias dentro de Blender a través del archivo __init__, que permite llamar al código que genera la geometría y crear un panel para retocar los atributos utilizados en su creación. También se ha visto lo fácil que es realizar una instalación y la distribución del mismo, sólo es necesario crear un archivo .zip. Esto demuestra las capacidades de personalización que Blender posé. El uso de tuberías normalizas, es también muy interesante, porque al incluir esta información se facilita la labor del ingeniero en el modelado, pudiéndose evitar errores al realizar el diseño con tuberías de diámetros no fabricados. También se ha podido ver la capacidad que tiene Blender de crear materiales fotorrealistas. Los nodos utilizados para crear estos materiales pueden verse en el apartado siguiente. 5. Puertos En este apartado se analizan dos formas diferentes de generar elementos para un modelo, por un lado, se ahonda en el concepto de la parametrización de piezas ya introducido en apartado anterior; y por otro lado mediante la generación procedimental de objetos, que permite la creación de elementos de naturaleza aleatoria. 5.1. Escollera natural (Generación procedimental) Como ya se mencionó en la introducción del documento, la ingeniería de caminos utiliza, a diferencia de otros campos de la ingeniería, además de piezas artificiales de geometrías conocidas, elementos naturales como una roca, que son de geometría aleatoria e imperrfecta. Por ello, es necesaria una metodología que permita la creación de dichos elementos. Y aunque a través de la programación se puede otorgar aleatoriedad a parámetros de una pieza parametrizada (lo cual se utiliza también en este apartado), es de mayor interés la capacidad de generar objetos con una componente de aleatoriedad aún mayor a través de la generación procedimental (Procedural generation). Esta metodología es ampliamente utilizada por programas como Blender con múltiples objetivos, crear terrenos sin poseer datos altimétricos, texturas, nubes, etc. permitiendo producir elementos que en la realidad son imperfectos e irrepetibles. Desarrollo La escollera natural utilizada en los diques de puertos suele colocarse por capas con rangos de tamaños, de manera que se van colocando capas de menor a mayor tamaño desde el interior del dique hacia el exterior del mismo, con el objetivo de que cada capa haga de filtro de la anterior, protegiendo el dique de la perdida de material. Por tanto, será necesario en el programa definir, para la creación de cada roca, un determinado 52 orden de magnitud que indique sus dimensiones. Para hacer esto sencillo, la roca se generará partiendo de un cubo de volumen conocido (cubo primitivo), y los vértices de este serán movidos en el espacio desde su posición original de forma aleatoria para cada eje entre un rango de valores definido. Para crea el cubo se ha optado por el uso de un método de la API, en vez de la creación de la malla y el objeto como se ha realizado en anteriores apartados. bpy.ops.mesh.primitive_cube_add() # Se crea el cubo primitivo. objeto = bpy.context.object # Se selecciona el cubo creado. Para acceder y modificar las coordenadas de los vértices de un objeto existente: objeto.data.vertices[indice_del_vertice].co[indice_de_la_coordenada] *= \ numero_aleatorio Figura 25. a) cubo primitivo y b) cubo con vértices modificados de forma aleatoria. Es interesante también poder definir si la roca utilizada (el cubo modificado) es redondeada, acicular o plana, para ello se introduce un nuevo cambio en el cubo modificado, escalando este, o no, en función de la forma deseada de la roca. objeto.scale = (1.0, escala_Y, escala_Z) # Escalado de los ejes Y y Z. Valores # iguales a 1 darían una roca # redondeada; si la escala Y se mantiene # en 1 y la Z se reduce, la roca tendría # forma de laja; si tanto la escala Y # como la Z se reducen, la roca sería # acicular. 53 Figura 26. Cubo modificado: a) redondeado, b) plano (escala_Z=0,2) y c) acicular (escala_Y=0.2, escala_Z=0,2). Hasta el momento, simplemente se ha introducido cierta aleatoriedad y cambios de dimensión sobre un cubo, pero este no parece una roca. Para cambiar esto, es necesario primero incrementar el número de caras del cubo, que permitirán introducir cambios en un mayor número de vértices, es decir, permitirán una mayor irregularidad de la superficie de la malla. Esto se lleva a cabo con el modificador Subdivision surface ya utilizado en el apartado 3.1.2. Figura 27. Cubo modificado con malla de mayor densidad: a) redondeado, b) plano (escala_Z=0,2), c) acicular (escala_Y=0.2, escala_Z=0,2). Una vez se tiene la malla con un mayor número de vértices para dar irregularidad a esta, hay que aplicar nuevos desplazamientos aleatorios a los vértices. Para poder realizar esto en un rango de valores definido y que además el resultado de un aspecto real de imperfección o aleatoriedad, se aplica la técnica de generación procedimental. En este caso consiste en utilizar una imagen generada procedimentalmente que contiene ruido en un rango de valores definido, y utilizar esta como la textura usada por un modificador de tipo Displace también utilizado en el apartado 3.1.2. La aleatoriedad de los desplazamientos viene definida por medio de la textura creada. Sin embargo, no es necesario saber crear dichas texturas procedimentales, puesto que Blender, al ser esta técnica ampliamente utilizada para el diseño, incluye múltiples formas de generarla de forma automática. Estas texturas se crean matemáticamente, y algunas de las incluidas en Blender son: − Clouds. − Musgrave. − Stucci. − Voronoi. − Noise (ruido generado de forma totalmente aleatoria). Todas estas texturas a excepción de Voronoi y Noise tiene una propiedad que permite elegir el algoritmo 54 utilizado para generar el ruido base. Además, de esta propiedad cada método tiene sus propias propiedades que otorgan una gran flexibilidad a la hora de generar las texturas. De esta forma es necesario encontrar la combinación de valores que produce una textura que produzca desplazamientos en la malla de modo que parezca una roca. En el código creado (breakwater_rock), se han incluido tres de los métodos existentes, en primer lugar, el método Clouds que genera ruido de tipo Perlin, en segundo lugar, el método Voronoi y por último, el método Stucci que como indica la documentación de Blender «es a menudo usado para rocas, asfalto...». Figura 28. Muestras de ruido generado de forma procedimental, métodos: a) Clouds, b) Voronoi y c) Stucci. Para la creación de las texturas, por tanto, se utiliza la API de Blender considerando para cada método utilizado los siguientes parámetros: # Para el método Clouds. textura = bpy.data.textures.new( nombre_de_la_textura, type='CLOUDS') # Crea la textura de tipo Clouds. textura.noise_basis = ruido_base # Define el algoritmo a utilizar para # generar el ruido. textura.noise_scale = escala # Definida de forma aleatoria. textura.noise_depth = profundidadS # Para el método Voronoi. textura = bpy.data.textures.new( nombre_de_la_textura, type='VORONOI') # Crea la textura de tipo Voronoi. textura.noise_intensity = intensidad # Definida de forma aleatoria. textura.noise_scale = escala # Definida de forma aleatoria. textura.weight_1 = peso_1 textura.weight_2 = peso_2 # Definido de forma aleatoria. # Para el método Stucci. textura = bpy.data.textures.new( nombre_de_la_textura, type='STUCCI') # Crea la textura de tipo Stucci. textura.noise_basis = ruido_base # Define el algoritmo a utilizar para 55 # generar el ruido. El resto de las propiedades quedarían con los valores por defecto. Siendo la textura creada la introducida en el modificador Displace. Además, se ha añadido la capacidad de crear un material para la roca, para ello. Se utiliza el siguiente árbol de nodos: Figura 29. Estructura del material creado para las rocas. En este caso se introducen los valores del nodo Principled BSDF a través de imágenes, mediante nodos del tipo Image Texture como las que se pueden ver a continuación: 56 Figura 30. Imágenes utilizadas para definir el material de las rocas. a) color, b) desplazamiento c) normal y d) rugosidad. Resultados y uso de la herramienta creada Para utilizar el código creado (breakwater_rock), se crea el objeto roca y se llama al método blendify del objeto: material = RockMaterial( base_color_filepath, roughness_filepath, normal_filepath, displacement_filepath) # Objeto que contiene las rutas a las imágenes para crear un # material. roca = NaturalRockFill( material=None, min_weight=1.0, max_weight=10.0, density=2.65, y_scale=1.0, z_scale=1.0, levels=4, noise_type='CLOUDS', noise_basis='ORIGINAL_PERLIN', strength=0.2, seed=0) # Al modificar los atributos se obtienen distintos tipos de roca. El # atributo material, es un objeto de la clase RockMaterial que # contiene únicamente las direcciones de los archivos que contiene las # imágenes utilizadas para la creación del material. Los atributos # min_weight y max_weight definen el rango de masas del cubo primitivo # con el que se creará la roca, teniendo en cuenta la densidad # (density). Modificando los atributos y_scale y z_scale, se obtienen # rocas: redondeadas, planas o aciculares. El atributo levels indica el # número de subdivisiones de la malla que se desean realizar. El # atributo noise_type indica el tipo de método con el que se va a # generar el ruido ('CLOUDS', 'VORONOI' y 'STUCCI'). El atributo # noise_basis indica el algoritmo a utilizar cuando se toma como # noise_type 'STUCCI' ('BLENDER_ORIGINAL', 'ORIGINAL_PERLIN', 57 # 'VORONOI_F1', 'VORONOI_F2', 'VORONOI_F3' y 'VORONOI_F4'). Strength # indica la proporción con la que se utiliza el ruido. El atributo seed, # permite generar una semilla para la generación de los números # aleatorios usados en el código para conseguir así poder repetir # los resultados. roca.blendify() # Se crea la roca en Blender. A continuación, se muestran una serie de rocas generadas por los distintos métodos utilizados. Figura 31. Rocas generadas mediante ruido procedimental. De izquierda a derechas métodos: Clouds, Voronio y Stucci. De arriba abajo: z_scale=1, z_scale=0.4, y z_scale=0.4 con material. Es posible generar el talud del dique de forma automática, así como sus distintas capas, mediante un simple bucle. En las siguientes figuras se muestra como quedaría una capa de un dique. 64 7. Conclusiones Con este trabajo se ha podido comprobar la viabilidad de utilizar Blender, un programa gratuito de diseño tridimensional, como herramienta de modelado en la ingeniería de caminos, canales y puertos. Este software permite obtener modelos tanto 3D como 2D. Como conclusiones se pueden mencionar a nivel de programación: - Se comprueba la posibilidad de usar fuentes de datos muy variadas, mediante el soporte directo de las mismas o con la creación de programas para la importación y exportación de estas, permitiéndose así la compatibilidad con otros programas. - Con la creación de un complemento y la capacidad de utilizar complementos de terceros se demuestra la capacidad prácticamente ilimitada de personalización del entorno de trabajo. - En general los códigos demuestran la capacidad de automatización de procesos de forma sencilla que tiene un programa como Blender mediante un lenguaje de programación ampliamente utilizado como Python o mediante la programación visual. El uso de Python, permite también el acceso a multitud de librerías reduciendo los costes de la programación puesto que en ocasiones la herramienta o parte de la herramienta que se quiere realizar ha sido ya creada. - Para el aprendizaje del programa y la API se ha utilizado como fuente de información la documentación de Blender, y conocidos fórums de programación como Stack Overflow, lo que demuestra que existen en internet fuentes de información de calidad, actualizadas y gratuitas para Blender, así como un gran número de usuarios. Siendo este último aspecto indispensable para decidir utilizar un programa. En cuanto a sus capacidades específicas para a ingeniería: - Se ha podido comprobar también la capacidad de introducir elementos propios de la ingeniería de caminos como modelos del terreno de grandes dimensiones, y las capacidades que tiene Blender para reducir los costes computacionales del modelo. - Además de los modelos del terreno se ha demostrado la posibilidad de crear elementos constructivos naturales e imperfectos como las rocas. - Se comprueba también la posibilidad de obtener resultados fotorrealistas. - Debe tenerse en cuenta que la curva de aprendizaje en un programa de diseño como Blender no es sencilla al ser generados los modelos de forma diferente a la herramienta CAD tradicional, y por incluir este una gran variedad de herramientas que en un principio pueden parecer no ser de utilidad para el ingeniero. Sin embargo, con respecto a esto último, la posibilidad de generar películas, modelos en movimientos, simulaciones, texturas o simplemente renderizados amplían las capacidades del ingeniero a la hora de captar clientes, y ayudan a una mejor comunicación y comprensión de los proyectos, siendo por tanto un aspecto positivo para aquel que quiera aportar mejoras a sus proyectos. 65 66 Bibliografía [1] TON ROOSENDAAL, ET AL. Blender, 2000-2021. https://www.blender.org/ [2] BLENDER DOCUMENTATION TEAM. The Blender 2.92 Manual. https://docs.blender.org/manual/en/2.92/ [3] MARTIN ISENBURG AND JONATHAN SHEWCHUK. LAStools. https://rapidlasso.com/lastools/ [4] JONES E, OLIPHANT E, PETERSON P, ET AL. SciPy: Open Source Scientific Tools for Python, 2001-2021, http://www.scipy.org/ [5] CLARK A, AND CONTRIBUTORS. Pilow (PIL Fork), 2010-2021, https://pillow.readthedocs.io/ [6] INSTITUTO GEOGRÁFICO NACIONAL. IGN. "http://centrodedescargas.cnig.es/CentroDescargas/index.jsp" [7] JONES E, OLIPHANT E, PETERSON P, ET AL. NumPy: Open Source Scientific Tools for Python, 1995-2021, https://numpy.org/ 67 68 ANEJO I: Caminos 69 Modelo digital del terreno Dependencias externas: − Scipy − Pillow Pil − numpy terrain_creator.py from scipy.spatial import Delaunay 1 from mathutils import Vector 2 from enum import Enum 3 from PIL import ( 4 Image, 5 TiffTags 6 ) 7 import numpy as np 8 import bpy 9 import os 10 11 12 Image.MAX_IMAGE_PIXELS = None # Cancel: Decompression bomb error. 13 14 15 UNIT = { 16 0: 0.0, 9001: 1, 9002: 0.3048, 9003: 0.304800609601219, 9005: 0.3047972654, 17 9014: 1.8288, 9030: 1852, 9031: 1.0000135965, 9033: 20.1168402336805, 18 9034: 0.201168402336805, 9035: 1609.34721869444, 9036: 1000.0, 19 9037: 0.9143917962, 9038: 20.1166195164, 9039: 0.201166195164, 20 9040: 0.914398414616029, 9041: 0.304799471538676, 9042: 20.1167651215526, 21 9043: 0.201167651215526, 9050: 0.9143992, 9051: 0.304799733333333, 22 9052: 20.1167824, 9053: 0.201167824, 9060: 0.914399204289812, 23 9061: 0.304799734763271, 9062: 20.1167824943759, 9063: 0.201167824943759, 24 9070: 0.304800833333333, 9080: 0.304799510248147, 9081: 0.30479841, 25 9082: 0.3047996, 9083: 0.3047995, 9084: 0.914398530744441, 9085: 0.91439523, 26 9086: 0.9143988, 9087: 0.9143985, 9093: 1609.344, 9094: 0.304799710181509, 27 9095: 0.3048007491, 9096: 0.9144, 9097: 20.1168, 9098: 0.201168, 28 9099: 0.914398, 9101: 1.0, 9102: 0.0174532925199433, 9103: 0.000290888208665721, 29 9104: 4.84813681109535e-006, 9105: 0.015707963267949, 9107: 0, 9108: 0, 30 9109: 1e-006, 9110: 0, 9111: 0, 9112: 0.000157079632679489, 31 9113: 1.5707963267949e-006, 9114: 0.000981747704246809, 9115: 0, 32 9116: 0, 9117: 0, 9118: 0, 9119: 0, 9120: 0, 9121: 0, 9122: 0.0174532925199433, 33 9300: 0.304799333333333, 9301: 20.116756, 9302: 0.20116756 34 } 35 36 LENGHT_MULTIPLES = { 37 'METRIC': { 38 'ADAPTATIVE': (None, ''), 39 'KILOMETERS': (1 / 1000, 'km'), 40 'METERS': (1, 'm'), 41 'CENTIMETERS': (100, 'cm'), 42 'MILIMETERS': (1000, 'mm'), 43 'MICROMETERS': (1000000, '\u00b5m') 44 }, 45 'IMPERIAL': { 46 70 'ADAPTATIVE': (None, ''), 47 'MILES': (1 / 5280, 'mi'), 48 'FEET': (1, '\''), 49 'INCHES': (12, '"'), 50 'THOU': (12000, 'thou') 51 } 52 } 53 54 COLOR_RAMPS = { 55 'RGB': [ 56 {'position': 0.000, 'color': (1.000, 0.200, 0.300, 1.000)}, 57 {'position': 0.500, 'color': (0.300, 0.200, 1.000, 1.000)}, 58 {'position': 1.000, 'color': (0.300, 1.000, 0.100, 1.000)} 59 ], 60 'ELEVATION': [ 61 {'position': 0.000, 'color': (0.094, 0.443, 0.027, 1.000)}, 62 {'position': 0.166, 'color': (0.427, 0.756, 0.247, 1.000)}, 63 {'position': 0.333, 'color': (0.521, 0.921, 0.117, 1.000)}, 64 {'position': 0.500, 'color': (0.949, 0.843, 0.552, 1.000)}, 65 {'position': 0.666, 'color': (0.682, 0.533, 0.274, 1.000)}, 66 {'position': 0.833, 'color': (0.458, 0.266, 0.086, 1.000)}, 67 {'position': 1.000, 'color': (1.000, 0.933, 0.925, 1.000)} 68 ] 69 } 70 71 72 class OriginPosition(Enum): 73 UPPER_LEFT_CELL_UPPER_LEFT_CORNER = 1 74 UPPER_LEFT_CENTER = 2 75 LOWER_LEFT_CELL_LOWER_LEFT_CORNER = 3 76 LOWER_LEFT_CENTER = 4 77 78 79 def lenght_conversion(lenght_epsg_code): 80 scene_unit_system = bpy.context.scene.unit_settings.system 81 scene_length_units = bpy.context.scene.unit_settings.length_unit 82 83 if scene_unit_system == 'METRIC': 84 return UNIT[lenght_epsg_code] * \ 85 LENGHT_MULTIPLES[scene_unit_system][scene_length_units][0] 86 87 elif scene_unit_system == 'IMPERIAL': 88 return UNIT[lenght_epsg_code] * 1 / 0.3048 * \ 89 LENGHT_MULTIPLES[scene_unit_system][scene_length_units][0] 90 91 92 class BBox(): 93 def __init__(self, obj): 94 b_box = [obj.matrix_world @ Vector(p) for p in obj.bound_box] 95 self.x_min = min([p[0] for p in b_box]) 96 self.x_max = max([p[0] for p in b_box]) 97 self.y_min = min([p[1] for p in b_box]) 98 self.y_max = max([p[1] for p in b_box]) 99 self.z_min = min([p[2] for p in b_box]) 100 self.z_max = max([p[2] for p in b_box]) 101 102 103 def sizes(self): 104 return ( 105 71 self.x_max - self.x_min, 106 self.y_max - self.y_min, 107 self.z_max - self.z_min 108 ) 109 110 def __repr__(self): 111 return """x_min = {0} 112 x_max = {1} 113 y_min = {2} 114 y_max = {3} 115 z_min = {4} 116 z_max = {5}\n""".format(self.x_min, self.x_max, self.y_min, self.y_max, 117 self.z_min, self.z_max) 118 119 120 def reposition_3D_view(b_box, orthographic=True): 121 max_size = int(max(b_box.sizes())) 122 for area in bpy.context.screen.areas: 123 if area.type == 'VIEW_3D': 124 for space in area.spaces: 125 if space.type == 'VIEW_3D': 126 if orthographic: 127 space.region_3d.view_perspective = 'ORTHO' 128 if max_size > 100: 129 space.clip_start = 1 130 if max_size > 1000: 131 space.clip_start = 10 132 space.clip_end = max_size * 5 133 134 ctx = bpy.context.copy() 135 ctx['area'] = area 136 ctx['region'] = area.regions[-1] 137 a = bpy.ops.view3d.view_selected(ctx) 138 139 140 class Dem(): 141 def __init__(self, filepath, mode='DELAUNAY', name='DEM', col_name='DEMs', 142 origin_zero=False, smooth=False): 143 self.filepath = filepath 144 self.mode = mode 145 self.name = name 146 self.col_name = col_name 147 self.origin_zero = origin_zero 148 self.smooth = smooth 149 150 def _voronoi_blendify(self, points, reposition=True): 151 bpy.ops.object.select_all(action='DESELECT') 152 mesh = bpy.data.meshes.new(self.name) 153 obj = bpy.data.objects.new(mesh.name, mesh) 154 if self.col_name in [c.name for c in bpy.data.collections]: 155 col = bpy.data.collections.get(self.col_name) 156 else: 157 col = bpy.data.collections.new(self.col_name) 158 bpy.context.scene.collection.children.link(col) 159 col.objects.link(obj) 160 bpy.context.view_layer.objects.active = obj 161 obj.select_set(True) 162 mesh.from_pydata(points, [], Delaunay(points[:, 0:-1]).simplices.tolist()) 163 164 72 if reposition: 165 reposition_3D_view(BBox(obj)) 166 167 if self.smooth: 168 bpy.ops.object.shade_smooth() 169 170 def _modifier_blendify(self, num_cols, num_rows, cell_size_x, cell_size_y, 171 origin_coords, origin_position, reposition=True): 172 bpy.ops.object.select_all(action='DESELECT') 173 174 # Mesh creation. 175 mesh = bpy.data.meshes.new(self.name) 176 if self.origin_zero: 177 x_2 = num_cols / 2 * cell_size_x 178 y_2 = num_rows / 2 * cell_size_y 179 verts = np.array(( 180 (-x_2, -y_2, 0), 181 (x_2, -y_2, 0), 182 (x_2, y_2, 0), 183 (-x_2, y_2, 0))) 184 else: 185 x = num_cols * cell_size_x 186 y = num_rows * cell_size_y 187 verts = np.array(( 188 (self.origin_coords[0], self.origin_coords[1] - y, 0), 189 (self.origin_coords[0] + x, self.origin_coords[1] - y, 0), 190 (self.origin_coords[0] + x, self.origin_coords[1], 0), 191 (self.origin_coords[0], self.origin_coords[1], 0))) 192 faces = [[0, 1, 2, 3]] 193 mesh.from_pydata(verts, [], faces) 194 195 # Object creation. 196 bpy.ops.object.select_all(action='DESELECT') 197 obj = bpy.data.objects.new(self.name, mesh) 198 if self.col_name in [c.name for c in bpy.data.collections]: 199 col = bpy.data.collections.get(self.col_name) 200 else: 201 col = bpy.data.collections.new(self.col_name) 202 bpy.context.scene.collection.children.link(col) 203 col.objects.link(obj) 204 bpy.context.view_layer.objects.active = obj 205 obj.select_set(True) 206 207 # Create UV layer. 208 uv_map = mesh.uv_layers.new(name='UvMap') 209 uv_map.data[0].uv = [ 210 cell_size_x / (num_cols * cell_size_x * 2), 211 cell_size_y / (num_rows * cell_size_y * 2)] 212 uv_map.data[1].uv = [ 213 1 - cell_size_x / (num_cols * cell_size_x * 2), 214 cell_size_y / (num_rows * cell_size_y * 2)] 215 uv_map.data[2].uv = [ 216 1 - cell_size_x / (num_cols * cell_size_x * 2), 217 1-cell_size_y / (num_rows * cell_size_y * 2)] 218 uv_map.data[3].uv = [ 219 cell_size_x / (num_cols * cell_size_x * 2), 220 1-cell_size_y / (num_rows * cell_size_y * 2)] 221 222 # Create subdivision surface modifier. 223 73 subivision_surface_mod = obj.modifiers.new('SUBDIVISION 1', type='SUBSURF') 224 subivision_surface_mod.subdivision_type = 'SIMPLE' 225 subivision_surface_mod.levels = 1 226 subivision_surface_mod.render_levels = 6 227 228 subivision_surface_mod = obj.modifiers.new('SUBDIBISION 2', type='SUBSURF') 229 subivision_surface_mod.subdivision_type = 'SIMPLE' 230 subivision_surface_mod.levels = 6 231 subivision_surface_mod.render_levels = 6 232 233 # Create displace modifier. 234 dem_image = bpy.data.images.load(self.filepath) 235 dem_image.name = 'DEM IMAGE' 236 dem_image.colorspace_settings.name = 'Non-Color' 237 238 dem_texture = bpy.data.textures.new('DEM HEIGHT TEXTURE', type='IMAGE') 239 dem_texture.image = dem_image 240 dem_texture.use_interpolation = True 241 dem_texture.extension = 'CLIP' 242 dem_texture.use_clamp = False 243 244 displace_mod = obj.modifiers.new('DISPLACE', type='DISPLACE') 245 displace_mod.texture = dem_texture 246 displace_mod.texture_coords = 'UV' 247 displace_mod.uv_layer = uv_map.name 248 displace_mod.mid_level = 0 249 250 if reposition: 251 reposition_3D_view(BBox(obj)) 252 253 if self.smooth: 254 bpy.ops.object.shade_smooth() 255 256 def set_rc_material(self, color_ramp='RGB'): 257 color_ramp = COLOR_RAMPS[color_ramp] 258 obj = bpy.context.view_layer.objects.active 259 b_box = BBox(obj) 260 261 # Create Normalize nodes group. 262 normalize_node_group = bpy.data.node_groups.new('Normalize', 'ShaderNodeTree') 263 normalize_node_group.nodes.new('NodeGroupInput') 264 normalize_node_group.inputs.new('NodeSocketFloat','val') 265 normalize_node_group.inputs.new('NodeSocketFloat','min') 266 normalize_node_group.inputs.new('NodeSocketFloat','max') 267 normalize_node_group.nodes.new('NodeGroupOutput') 268 normalize_node_group.outputs.new('NodeSocketFloat', 'val') 269 270 normalize_nodes = normalize_node_group.nodes 271 normalize_nodes.new('ShaderNodeMath') # Math -> First subtract. 272 normalize_nodes.new('ShaderNodeMath') # Math.001 -> Second subtract. 273 normalize_nodes.new('ShaderNodeMath') # Math.002 -> Divide. 274 275 normalize_nodes['Math'].operation = 'SUBTRACT' 276 normalize_nodes['Math.001'].operation = 'SUBTRACT' 277 normalize_nodes['Math.002'].operation = 'DIVIDE' 278 279 normalize_nodes['Group Input'].location = (-340.0, 40.0) 280 normalize_nodes['Math'].location = (-160.0, 160.0) 281 normalize_nodes['Math.001'].location = (-160.0, -20.0) 282 80 Extracción de perfiles longitudinales de una carretera Dependencias externas: − Pillow Pil − numpy longitudinal.py from PIL import Image, TiffTags 630 from math import pi as PI 631 from math import sqrt 632 from enum import Enum 633 from copy import copy 634 import numpy as np 635 import bpy 636 import os 637 638 639 class OriginPosition(Enum): 640 UPPER_LEFT_CELL_UPPER_LEFT_CORNER = 1 641 UPPER_LEFT_CENTER = 2 642 LOWER_LEFT_CELL_LOWER_LEFT_CORNER = 3 643 LOWER_LEFT_CENTER = 4 644 645 646 class Point2D(): 647 def __init__(self, x, y): 648 self.x = x 649 self.y = y 650 651 @ classmethod 652 def from_array(cls, a): 653 return cls(a[0], a[1]) 654 655 @ classmethod 656 def from_vector2D(cls, v, k): 657 return cls(v.origin.x + v.x * k , v.origin.y + v.y * k) 658 659 def __copy__(self): 660 return type(self)(self.x, self.y) 661 662 def __repr__(self): 663 return "{x:<10.4f} {y:<10.4f}".format(x=self.x, y=self.y) 664 665 666 class Vector2D(Point2D): 667 def __init__(self, x, y, origin=Point2D(0, 0)): 668 super().__init__(x, y) 669 self.origin = origin 670 671 @classmethod 672 def from_points(cls, p1, p2, set_origin=True): 673 if set_origin: 674 return cls(p2.x - p1.x, p2.y - p1.y, p1) 675 else: 676 81 return cls(p2.x - p1.x, p2.y - p1.y) 677 678 def module(self): 679 return sqrt(self.x**2 + self.y**2) 680 681 def unitary(self): 682 m = self.module() 683 return Vector2D(self.x / m, self.y / m, self.origin) 684 685 686 class Profile(): 687 def __init__(self, points, raster, initial_x=0): 688 self.num_points = len(points) 689 self.initial_x = initial_x 690 self.x = np.zeros(self.num_points) # Distance. 691 self.y = np.zeros(self.num_points) # Height. 692 metadata_dict = {key : raster.tag[key] for key in raster.tag.keys()} 693 self.raster_cell_size_x = metadata_dict[33550][0] 694 self.raster_cell_size_y = metadata_dict[33550][1] 695 self.raster_origin = Point2D.from_array(metadata_dict[33922][3:5]) 696 self.y[0] = raster.getpixel( 697 ((points[0].x - self.raster_origin.x) / self.raster_cell_size_x, 698 (self.raster_origin.y - points[0].y) / self.raster_cell_size_y)) 699 for i in range(1, self.num_points): 700 self.x[i] = self.x[i - 1] + Vector2D.from_points( 701 points[i - 1], points[i]).module() 702 self.y[i] = raster.getpixel( 703 ((points[i].x - self.raster_origin.x) / self.raster_cell_size_x, 704 (self.raster_origin.y - points[i].y) / self.raster_cell_size_y)) 705 706 @classmethod 707 def by_paths(cls, csv_filepath, raster_filepath, create_new_points=False): 708 csv = np.loadtxt(csv_filepath, delimiter=',', skiprows=1, usecols=(1, 2, 3)) 709 710 raster = Image.open(raster_filepath) 711 metadata_dict = {key : raster.tag[key] for key in raster.tag.keys()} 712 cell_size_x = metadata_dict[33550][0] 713 cell_size_y = metadata_dict[33550][1] 714 min_cell_size = min(cell_size_x, cell_size_y) 715 716 points = [] 717 718 last_point = Point2D.from_array(csv[0, :-1]) 719 points.append(last_point) 720 for i in range(1, csv.shape[0]): 721 p = Point2D.from_array(csv[i, :-1]) 722 v = Vector2D.from_points(last_point, p) 723 m = v.module() 724 if m > min_cell_size: 725 num_new_points = int(m / min_cell_size) + 1 726 step = m / num_new_points 727 else: 728 num_new_points = 0 729 730 if num_new_points and create_new_points: 731 vu = v.unitary() 732 for j in range(num_new_points): 733 points.append(Point2D.from_vector2D(vu, (j + 1) * step)) 734 else: 735 82 points.append(p) 736 last_point = copy(p) 737 738 return cls(points, raster) 739 740 741 class TickProperty(): 742 def __init__(self, axis, spacing='DEFAULT', axis_spacing='DEFAULT', 743 scale='DEFAULT', overflow_lenght='DEFAULT', color=None): 744 if isinstance(spacing, str): 745 if axis.upper() == 'X': 746 self.spacing = 250 747 else: 748 self.spacing = 10 749 else: 750 self.spacing = spacing 751 if isinstance(axis_spacing, str): 752 if axis_spacing.upper() == 'DEFAULT': 753 self.axis_spacing = 5 754 elif axis_spacing.upper() == 'AUTO': 755 self.axis_spacing = 'AUTO' 756 else: 757 self.axis_spacing = axis_spacing 758 if isinstance(scale, str): 759 if scale.upper() == 'DEFAULT': 760 self.scale = 10 761 elif scale.upper() == 'AUTO': 762 self.scale = 'AUTO' 763 else: 764 self.scale = scale 765 if isinstance(overflow_lenght, str): 766 if overflow_lenght.upper() == 'DEFAULT': 767 self.overflow_lenght = 2.5 768 elif overflow_lenght.upper() == 'AUTO': 769 self.overflow_lenght = 'AUTO' 770 else: 771 self.overflow_lenght = overflow_lenght 772 self.color = color 773 774 def _set_axis_spacing(self, x_range, y_range): 775 self.axis_spacing = min(x_range[1] - x_range[0], 776 y_range[1] - y_range[0]) / 20 777 print('[5] axis_spacing ==', self.axis_spacing) 778 779 def _set_scale(self, x_range, y_range): 780 self.scale = min(x_range[1] - x_range[0], 781 y_range[1] - y_range[0]) / 10 782 print('[10] scale ==', self.scale) 783 784 def _set_overflow_lenght(self, x_range, y_range): 785 self.overflow_lenght = min(x_range[1] - x_range[0], 786 y_range[1] - y_range[0]) / 40 787 print('[2.5] overflow_lenght ==', self.overflow_lenght) 788 789 def _revise(self, x_range, y_range): 790 if self.axis_spacing == 'AUTO': 791 self._set_axis_spacing(x_range, y_range) 792 if self.scale == 'AUTO': 793 self._set_scale(x_range, y_range) 794 83 if self.overflow_lenght == 'AUTO': 795 self._set_overflow_lenght(x_range, y_range) 796 797 798 class ProfilePloter(): 799 def __init__(self, profile, x_ticks_props, y_ticks_props, x_scale=0.1, 800 y_scale=1, first_pk=0, origin=(0, 0)): 801 self.profile = profile 802 self.x_ticks_props = x_ticks_props 803 self.y_ticks_props = y_ticks_props 804 self.x_scale = x_scale 805 self.y_scale = y_scale 806 self.first_pk = first_pk 807 self.origin = origin 808 809 self.x_range = (0, max(self.profile.x)) 810 self.y_range = ( 811 min(self.profile.y) - 10 if min(self.profile.y) - 10 > 0 else 0, 812 max(self.profile.y) + 10) 813 self.num_x_ticks = abs(abs( 814 self.x_range[1]) - abs(self.x_range[0])) // self.x_ticks_props.spacing + 1 815 self.num_y_ticks = abs(abs( 816 self.y_range[1]) - abs(self.y_range[0])) // self.y_ticks_props.spacing + 1 817 self.x_ticks = np.hstack(( 818 self.x_range[0] + np.arange(0, self.num_x_ticks) * \ 819 self.x_ticks_props.spacing, self.x_range[1])) 820 self.num_x_ticks += 1 821 self.y_ticks = self.y_range[0] + np.arange(0, self.num_y_ticks) * \ 822 self.y_ticks_props.spacing 823 824 self.x_ticks_props._revise(self.x_range, self.y_range) 825 self.y_ticks_props._revise(self.x_range, self.y_range) 826 827 def _get_pk(self, x, num_decimals=2, decimal_separator=','): 828 return '{0:.0f}+{1:0{2}.{3}f}'.format( 829 x // 1000, x % 1000, num_decimals + 4 if num_decimals > 0 else 3, 830 num_decimals).replace('.', decimal_separator) 831 832 def _get_elevation(self, y, num_decimals=2, decimal_separator=','): 833 return '{0:.{1}f}'.format(y, num_decimals).replace('.', decimal_separator) 834 835 def blendify(self): 836 col = bpy.data.collections.new('Profile') 837 bpy.context.scene.collection.children.link(col) 838 col_name = col.name 839 840 fon = bpy.data.fonts.load(r'C:\WINDOWS\Fonts\verdana.ttf') 841 842 # box. 843 cur = bpy.data.curves.new(name='Box', type='CURVE') 844 cur.dimensions = '2D' 845 cur_spline = cur.splines.new('POLY') 846 cur_spline.points.add(3) 847 cur_spline.points[0].co = (self.origin[0], self.origin[1], 0.0, 0.0) 848 cur_spline.points[1].co = ( 849 self.origin[0] + (self.x_range[1] - self.x_range[0]) * self.x_scale, 850 self.origin[1], 0.0, 0.0) 851 cur_spline.points[2].co = ( 852 self.origin[0] + (self.x_range[1] - self.x_range[0]) * self.x_scale, 853 84 self.origin[1] + (self.y_range[1] - self.y_range[0]) * self.y_scale, 854 0.0, 0.0) 855 cur_spline.points[3].co = ( 856 self.origin[0], self.origin[1] + (self.y_range[1] - self.y_range[0]) * \ 857 self.y_scale, 0.0, 0.0) 858 cur_spline.use_cyclic_u = True 859 860 obj = bpy.data.objects.new('Box', cur) 861 bpy.data.collections[col_name].objects.link(obj) 862 bpy.context.view_layer.objects.active = obj 863 864 # x_ticks. 865 cur = bpy.data.curves.new(name='X Ticks', type='CURVE') 866 cur.dimensions = '2D' 867 for x_tick in self.x_ticks: 868 cur_spline = cur.splines.new('POLY') 869 cur_spline.points.add(1) 870 cur_spline.points[0].co = ( 871 self.origin[0] + (x_tick - self.x_range[0]) * self.x_scale, 872 self.origin[1] - self.x_ticks_props.overflow_lenght, 0.0, 0.0) 873 cur_spline.points[1].co = ( 874 self.origin[0] + (x_tick - self.x_range[0]) * self.x_scale, 875 self.origin[1] + (self.y_range[1] - self.y_range[0]) * self.y_scale, 876 0.0, 0.0) 877 878 obj = bpy.data.objects.new('X Ticks', cur) 879 bpy.data.collections[col_name].objects.link(obj) 880 bpy.context.view_layer.objects.active = obj 881 882 # x_labels. 883 for x_tick in self.x_ticks: 884 cur = bpy.data.curves.new(type="FONT", name="FF") 885 cur.body = self._get_pk(x_tick + self.first_pk) 886 cur.font = fon 887 cur.align_x = 'RIGHT' 888 cur.align_y = 'CENTER' 889 obj = bpy.data.objects.new(name="Font Object", object_data=cur) 890 bpy.data.collections[col_name].objects.link(obj) 891 bpy.context.view_layer.objects.active = obj 892 obj.rotation_euler = (0.0, 0.0, PI/2) 893 obj.location = ( 894 self.origin[0] + (x_tick - self.x_range[0]) * self.x_scale, 895 self.origin[1] - self.x_ticks_props.axis_spacing, 0.0) 896 obj.scale = ( 897 self.x_ticks_props.scale, self.x_ticks_props.scale, 898 self.x_ticks_props.scale) 899 900 # y_ticks. 901 cur = bpy.data.curves.new(name='Y Ticks', type='CURVE') 902 cur.dimensions = '2D' 903 for y_tick in self.y_ticks: 904 cur_spline = cur.splines.new('POLY') 905 cur_spline.points.add(1) 906 cur_spline.points[0].co = ( 907 self.origin[0] - self.y_ticks_props.overflow_lenght, 908 self.origin[1] + (y_tick - self.y_range[0]) * self.y_scale, 0.0, 0.0) 909 cur_spline.points[1].co = ( 910 self.origin[0] + (self.x_range[1] - self.x_range[0]) * self.x_scale, 911 self.origin[1] + (y_tick - self.y_range[0]) * self.y_scale, 0.0, 0.0) 912 85 913 obj = bpy.data.objects.new('Y Ticks', cur) 914 bpy.data.collections[col_name].objects.link(obj) 915 bpy.context.view_layer.objects.active = obj 916 917 # y_labels. 918 for y_tick in self.y_ticks: 919 cur = bpy.data.curves.new(type="FONT", name="FF") 920 cur.body = self._get_elevation(y_tick) 921 cur.font = fon 922 cur.align_x = 'RIGHT' 923 cur.align_y = 'CENTER' 924 obj = bpy.data.objects.new(name="Font Object", object_data=cur) 925 bpy.data.collections[col_name].objects.link(obj) 926 bpy.context.view_layer.objects.active = obj 927 obj.location = ( 928 self.origin[0] - self.y_ticks_props.axis_spacing, 929 self.origin[1] + (y_tick - self.y_range[0]) * self.y_scale, 0.0) 930 obj.scale = ( 931 self.y_ticks_props.scale, self.y_ticks_props.scale, 932 self.y_ticks_props.scale) 933 934 # profile. 935 cur = bpy.data.curves.new(name='Profile', type='CURVE') 936 cur.dimensions = '2D' 937 cur_spline = cur.splines.new('BEZIER') 938 cur_spline.bezier_points.add(self.profile.num_points - 1) 939 cur_spline.bezier_points.foreach_set('co', 940 np.hstack(( 941 (self.origin[0] + (self.profile.x - self.x_range[0]) * \ 942 self.x_scale).reshape((self.profile.num_points, 1)), 943 (self.origin[1] + (self.profile.y - self.y_range[0]) * \ 944 self.y_scale).reshape((self.profile.num_points, 1)), 945 np.zeros((self.profile.num_points, 1)) 946 )).flatten()) 947 for i in range(self.profile.num_points): 948 cur_spline.bezier_points[i].handle_right_type = 'AUTO' 949 cur_spline.bezier_points[i].handle_left_type = 'AUTO' 950 951 obj = bpy.data.objects.new('Profile', cur) 952 bpy.data.collections[col_name].objects.link(obj) 953 bpy.context.view_layer.objects.active = obj 954 955 956 DIR_PATH = r"C:\Users\MIG17\Desktop\MIGUEL\ano7\TFM\codigos\312" 957 958 points_filepath = os.path.join(DIR_PATH, r".\points.csv") 959 raster_filepath = os.path.join(DIR_PATH, r".\elevation.tif") 960 961 962 if __name__ == '__main__': 963 pass 964 86 ANEJO II: Canales 87 Tuberías Estructura del archivo .zip: − data_materials: Esta carpeta debe contener las imágenes que definen el material del objeto que se desea crear (color, rugosidad, normales y desplazamiento). − __init__.py − add_pipe.py __init__.py bl_info = { 965 'name': 'Pipe', 966 'author': 'Miguel Puech de Oriol', 967 'version': (1, 0, 0), 968 'blender': (2, 92, 0), 969 'location': 'View3D > Add > Mesh', 970 'description': 'Adds a Pipe', 971 'warning': '', 972 'category': 'Add Mesh', 973 } 974 975 976 import bpy 977 from . import add_pipe 978 979 980 def register(): 981 add_pipe.register() 982 983 def unregister(): 984 add_pipe.unregister() 985 986 987 if __name__ == '__main__': 988 register() 989 88 add_pipe.py from math import pi as PI 990 from math import ( 991 sin, 992 cos 993 ) 994 from bpy_extras import ( 995 object_utils, 996 image_utils 997 ) 998 import bmesh 999 import bpy 1000 import os 1001 1002 1003 REINFORCED_CONCRETE_MEASUERES = { 1004 '400': (0.400, 0.065), '500': (0.500, 0.070), '600': (0.600, 0.080), 1005 '800': (0.800, 0.095), '1000': (1.000, 0.120), '1200': (1.200, 0.135), 1006 '1500': (1.500, 0.162), '1800': (1.800, 0.200), '2000': (2.000, 0.212), 1007 '2500': (2.500, 0.250), 1008 } 1009 1010 DUCTIL_IRON_MEASUERES = { 1011 '80': (0.080, 0.018), '100': (0.100, 0.018), '125': (0.125, 0.019), 1012 '150': (0.150, 0.020), '200': (0.200, 0.022), '250': (0.250, 0.024), 1013 '300': (0.300, 0.026), '350': (0.350, 0.028), '400': (0.400, 0.029), 1014 '450': (0.450, 0.030), '500': (0.500, 0.032), '600': (0.600, 0.035), 1015 '700': (0.700, 0.038), '800': (0.800, 0.042), '900': (0.900, 0.045), 1016 '1000': (1.000, 0.048), 1017 } 1018 1019 POLYETHYLENE_MEASUERES = { 1020 'LOW_DENSITY PE-40': { 1021 'PN-4': { 1022 '32': (0.032, 0.0020), '40': (0.040, 0.0024), '50': (0.050, 0.0030), 1023 '63': (0.063, 0.0038), '75': (0.075, 0.0045), '90': (0.090, 0.0054), 1024 }, 1025 'PN-6': { 1026 '20': (0.020, 0.0020), '25': (0.025, 0.0023), '32': (0.032, 0.0030), 1027 '40': (0.040, 0.0037), '50': (0.050, 0.0046), '63': (0.063, 0.0058), 1028 '75': (0.075, 0.0068), '90': (0.090, 0.0082), 1029 }, 1030 'PN-10': { 1031 '20': (0.020, 0.0030), '25': (0.025, 0.0035), '32': (0.032, 0.0044), 1032 '40': (0.040, 0.0055), '50': (0.050, 0.0069), '63': (0.063, 0.0086), 1033 '75': (0.075, 0.0103), '90': (0.090, 0.0123), 1034 } 1035 }, 1036 'HIGH_DENSITY PE-100': { 1037 'PN-10': { 1038 '50': (0.050, 0.0030), '63': (0.063, 0.0038), '75': (0.075, 0.0045), 1039 '90': (0.090, 0.0054), '110': (0.110, 0.0066), '125': (0.125, 0.0074), 1040 '140': (0.140, 0.0083), '160': (0.160, 0.0095), '180': (0.180, 0.0107), 1041 '200': (0.200, 0.0119), '250': (0.250, 0.0148), '315': (0.315, 0.0187), 1042 }, 1043 'PN-16': { 1044 89 '50': (0.050, 0.0046), '63': (0.063, 0.0058), '75': (0.075, 0.0068), 1045 '90': (0.090, 0.0082), '110': (0.110, 0.0100), '125': (0.125, 0.0114), 1046 '140': (0.140, 0.0127), '160': (0.160, 0.0146), '180': (0.180, 0.0164), 1047 '200': (0.200, 0.0182), '250': (0.250, 0.0227), '315': (0.315, 0.0286), 1048 } 1049 } 1050 } 1051 1052 POLYVINYL_CHLORIDE_MEASUERES = { 1053 'PN-6': { 1054 '75': (0.075, 0.0022), '90': (0.090, 0.0027), '110': (0.110, 0.0027), 1055 '125': (0.125, 0.0031), '140': (0.140, 0.0035), '160': (0.160, 0.0041), 1056 '180': (0.180, 0.0044), '200': (0.200, 0.0050), '250': (0.250, 0.0062), 1057 '315': (0.315, 0.0077), '400': (0.400, 0.0098), '500': (0.500, 0.0123), 1058 '630': (0.630, 0.0154), 1059 }, 1060 'PN-10': { 1061 '75': (0.075, 0.0036), '90': (0.090, 0.0043), '110': (0.110, 0.0042), 1062 '125': (0.125, 0.0048), '140': (0.140, 0.0054), '160': (0.160, 0.0062), 1063 '180': (0.180, 0.0070), '200': (0.200, 0.0077), '250': (0.250, 0.0096), 1064 '315': (0.315, 0.0121), 1065 }, 1066 'PN-16': { 1067 '75': (0.075, 0.0056), '90': (0.090, 0.0067), '110': (0.110, 0.0066), 1068 '125': (0.125, 0.0074), '140': (0.140, 0.0083), '160': (0.160, 0.0095), 1069 '180': (0.180, 0.0107), '200': (0.200, 0.0119), '250': (0.250, 0.0148), 1070 '315': (0.315, 0.0187), 1071 } 1072 } 1073 1074 def get_dbase(): 1075 return os.path.join(__file__.rstrip('add_pipe.py'), 'data_materials') 1076 1077 MATERIAL_DBASE = get_dbase() 1078 1079 REINFORCED_CONCRETE_MATERIAL = { 1080 'rc_base_color': os.path.join( 1081 MATERIAL_DBASE, 'Concrete030_2K_Color.jpg'), 1082 'rc_roughness': os.path.join( 1083 MATERIAL_DBASE, 'Concrete030_2K_Roughness.jpg'), 1084 'rc_normal': os.path.join( 1085 MATERIAL_DBASE, 'Concrete030_2K_Normal.jpg'), 1086 'rc_displacement': os.path.join( 1087 MATERIAL_DBASE, 'Concrete030_2K_Displacement.jpg') 1088 } 1089 1090 DUCTIL_IRON_MATERIAL = { # Black metal. 1091 'di_base_color': os.path.join( 1092 MATERIAL_DBASE, 'Metal027_2K_Color.jpg'), 1093 'di_roughness': os.path.join( 1094 MATERIAL_DBASE, 'Metal027_2K_Roughness.jpg'), 1095 'di_normal': os.path.join( 1096 MATERIAL_DBASE, 'Metal027_2K_Normal.jpg'), 1097 'di_displacement': os.path.join( 1098 MATERIAL_DBASE, 'Metal027_2K_Displacement.jpg') 1099 } 1100 1101 POLYETHYLENE_MATERIAL = { # Black plastic. 1102 'pe_base_color': os.path.join( 1103 96 items=( 1458 ('50', 'ND 50 mm', 'ND 50 mm'), ('63', 'ND 63 mm', 'ND 63 mm'), 1459 ('75', 'ND 75 mm', 'ND 75 mm'), ('90', 'ND 90 mm', 'ND 90 mm'), 1460 ('110', 'ND 110 mm', 'ND 110 mm'), ('125', 'ND 125 mm', 'ND 125 mm'), 1461 ('140', 'ND 140 mm', 'ND 140 mm'), ('160', 'ND 160 mm', 'ND 160 mm'), 1462 ('180', 'ND 180 mm', 'ND 180 mm'), ('200', 'ND 200 mm', 'ND 200 mm'), 1463 ('250', 'ND 250 mm', 'ND 250 mm'), ('315', 'ND 315 mm', 'ND 315 mm')), 1464 name='Diameter', 1465 description='Pipe diameter', 1466 default=0 1467 ) 1468 nominal_pressure_pvc: bpy.props.EnumProperty( 1469 items=( 1470 ('PN-6', 'PN-6', 'PN-6'), 1471 ('PN-10', 'PN-10', 'PN-10'), 1472 ('PN-16', 'PN-16', 'PN-16')), 1473 name='Pressure', 1474 description='Pipe nominal pressure', 1475 default=0 1476 ) 1477 nominal_diameter_pvc_pn6: bpy.props.EnumProperty( 1478 items=( 1479 ('75', 'ND 75 mm', 'ND 75 mm'), ('90', 'ND 90 mm', 'ND 90 mm'), 1480 ('110', 'ND 110 mm', 'ND 110 mm'), ('125', 'ND 125 mm', 'ND 125 mm'), 1481 ('140', 'ND 140 mm', 'ND 140 mm'), ('160', 'ND 160 mm', 'ND 160 mm'), 1482 ('180', 'ND 180 mm', 'ND 180 mm'), ('200', 'ND 200 mm', 'ND 200 mm'), 1483 ('250', 'ND 250 mm', 'ND 250 mm'), ('315', 'ND 315 mm', 'ND 315 mm'), 1484 ('400', 'ND 400 mm', 'ND 400 mm'), ('500', 'ND 500 mm', 'ND 500 mm'), 1485 ('630', 'ND 630 mm', 'ND 630 mm')), 1486 name='Diameter', 1487 description='Pipe diameter', 1488 default=0 1489 ) 1490 nominal_diameter_pvc_pn10: bpy.props.EnumProperty( 1491 items=( 1492 ('75', 'ND 75 mm', 'ND 75 mm'), ('90', 'ND 90 mm', 'ND 90 mm'), 1493 ('110', 'ND 110 mm', 'ND 110 mm'), ('125', 'ND 125 mm', 'ND 125 mm'), 1494 ('140', 'ND 140 mm', 'ND 140 mm'), ('160', 'ND 160 mm', 'ND 160 mm'), 1495 ('180', 'ND 180 mm', 'ND 180 mm'), ('200', 'ND 200 mm', 'ND 200 mm'), 1496 ('250', 'ND 250 mm', 'ND 250 mm'), ('315', 'ND 315 mm', 'ND 315 mm')), 1497 name='Diameter', 1498 description='Pipe diameter', 1499 default=0 1500 ) 1501 nominal_diameter_pvc_pn16: bpy.props.EnumProperty( 1502 items=( 1503 ('75', 'ND 75 mm', 'ND 75 mm'), ('90', 'ND 90 mm', 'ND 90 mm'), 1504 ('110', 'ND 110 mm', 'ND 110 mm'), ('125', 'ND 125 mm', 'ND 125 mm'), 1505 ('140', 'ND 140 mm', 'ND 140 mm'), ('160', 'ND 160 mm', 'ND 160 mm'), 1506 ('180', 'ND 180 mm', 'ND 180 mm'), ('200', 'ND 200 mm', 'ND 200 mm'), 1507 ('250', 'ND 250 mm', 'ND 250 mm'), ('315', 'ND 315 mm', 'ND 315 mm')), 1508 name='Diameter', 1509 description='Pipe diameter', 1510 default=0 1511 ) 1512 diameter: bpy.props.FloatProperty( 1513 name='Diameter', 1514 default=1, 1515 min=0.01, 1516 97 unit='LENGTH', 1517 precision=3 1518 ) 1519 thickness: bpy.props.FloatProperty( 1520 name='Thickness', 1521 default=0.1, 1522 min=0.001, 1523 max=100, 1524 unit='LENGTH', 1525 precision=4 1526 ) 1527 length: bpy.props.FloatProperty( 1528 name='Length', 1529 default=2, 1530 min=0.002, 1531 unit='LENGTH', 1532 precision=3 1533 ) 1534 resolution: bpy.props.IntProperty( 1535 name='Resolution', 1536 default=3, 1537 min=3, 1538 max=500 1539 ) 1540 1541 def draw(self, context): 1542 layout = self.layout 1543 col = layout.column() 1544 col.prop(self, 'normalized') 1545 col.prop(self, 'material_type') 1546 1547 box = layout.box() 1548 box_col = box.column(align=True) 1549 box_col.label(text='Measures:') 1550 1551 if self.normalized: 1552 if self.material_type == 'RC': 1553 col.prop(self, 'nominal_diameter_rc') 1554 elif self.material_type == 'DI': 1555 col.prop(self, 'nominal_diameter_di') 1556 elif self.material_type == 'PE': 1557 col.prop(self, 'density_pe') 1558 if self.density_pe == 'LOW_DENSITY PE-40': 1559 col.prop(self, 'nominal_pressure_pe_ld') 1560 if self.nominal_pressure_pe_ld == 'PN-4': 1561 col.prop(self, 'nominal_diameter_pe_ld_pn4') 1562 elif self.nominal_pressure_pe_ld == 'PN-6': 1563 col.prop(self, 'nominal_diameter_pe_ld_pn6') 1564 elif self.nominal_pressure_pe_ld == 'PN-10': 1565 col.prop(self, 'nominal_diameter_pe_ld_pn10') 1566 elif self.density_pe == 'HIGH_DENSITY PE-100': 1567 col.prop(self, 'nominal_pressure_pe_hd') 1568 if self.nominal_pressure_pe_hd == 'PN-10': 1569 col.prop(self, 'nominal_diameter_pe_hd_pn10') 1570 elif self.nominal_pressure_pe_hd == 'PN-16': 1571 col.prop(self, 'nominal_diameter_pe_hd_pn16') 1572 elif self.material_type == 'PVC': 1573 col.prop(self, 'nominal_pressure_pvc') 1574 if self.nominal_pressure_pvc == 'PN-6': 1575 98 col.prop(self, 'nominal_diameter_pvc_pn6') 1576 elif self.nominal_pressure_pvc == 'PN-10': 1577 col.prop(self, 'nominal_diameter_pvc_pn10') 1578 elif self.nominal_pressure_pvc == 'PN-16': 1579 col.prop(self, 'nominal_diameter_pvc_pn16') 1580 diameter, thickness = get_nominal_measures(self) 1581 if self.material_type in ('RC', 'DI'): 1582 box_col.label(text='Nominal diameter (Inner): ' + str(diameter) + ' m') 1583 else: 1584 box_col.label(text='Nominal diameter (Outer): ' + str(diameter) + ' m') 1585 box_col.label(text='Thickness: ' + str(thickness) + ' mm') 1586 else: 1587 box_col.prop(self, 'diameter') 1588 box_col.prop(self, 'thickness') 1589 1590 box_col.prop(self, 'length') 1591 layout = self.layout 1592 col = layout.column() 1593 col.prop(self, 'resolution') 1594 col.prop(self, 'align') 1595 col.prop(self, 'location') 1596 col.prop(self, 'rotation') 1597 1598 def execute(self, context): 1599 self.mat_name = '_'.join(('PIPE_MATERIAL', self.material_type)) 1600 if self.mat_name not in [mat.name for mat in bpy.data.materials]: 1601 # Create material. 1602 mat = bpy.data.materials.new(name=self.mat_name) 1603 self.mat_name = mat.name 1604 1605 mat.use_nodes = True 1606 mat_nodes = mat.node_tree.nodes 1607 1608 # Create material nodes. 1609 mat_nodes.new('ShaderNodeTexCoord') 1610 mat_nodes.new('ShaderNodeMapping') 1611 mat_nodes.new('ShaderNodeTexImage') # Base Colour. 1612 mat_nodes['Image Texture'].label = 'Base Color' 1613 mat_nodes.new('ShaderNodeTexImage') # Roughness. 1614 mat_nodes['Image Texture.001'].label = 'Roughness' 1615 mat_nodes.new('ShaderNodeTexImage') # Normal. 1616 mat_nodes['Image Texture.002'].label = 'Normal' 1617 mat_nodes.new('ShaderNodeTexImage') # Displacement. 1618 mat_nodes['Image Texture.003'].label = 'Displacement' 1619 mat_nodes.new('ShaderNodeNormalMap') 1620 mat_nodes.new('ShaderNodeDisplacement') 1621 1622 # Locate material nodes. 1623 mat_nodes['Texture Coordinate'].location = (-700.0, 125.0) 1624 mat_nodes['Mapping'].location = (-500.0, 175.0) 1625 mat_nodes['Image Texture'].location = (-300.0, 515.0) 1626 mat_nodes['Image Texture.001'].location = (-300.0, 255.0) 1627 mat_nodes['Image Texture.002'].location = (-300.0, -5.0) 1628 mat_nodes['Image Texture.003'].location = (-300.0, -265.0) 1629 mat_nodes['Normal Map'].location = (0.0, 0.0) 1630 mat_nodes['Displacement'].location = (275.0, -300.0) 1631 mat_nodes['Principled BSDF'].location = (225.0, 450.0) 1632 mat_nodes['Material Output'].location = (525.0, 50.0) 1633 1634 99 # Load material images. 1635 image_names = [im.name for im in bpy.data.images] 1636 if 'rc_base_color' not in image_names: 1637 img = image_utils.load_image( 1638 REINFORCED_CONCRETE_MATERIAL['rc_base_color']) 1639 img.name = 'rc_base_color' 1640 if 'rc_roughness' not in image_names: 1641 img = image_utils.load_image( 1642 REINFORCED_CONCRETE_MATERIAL['rc_roughness']) 1643 img.name = 'rc_roughness' 1644 img.colorspace_settings.name = 'Non-Color' 1645 if 'rc_normal' not in image_names: 1646 img = image_utils.load_image( 1647 REINFORCED_CONCRETE_MATERIAL['rc_normal']) 1648 img.name = 'rc_normal' 1649 img.colorspace_settings.name = 'Non-Color' 1650 if 'rc_displacement' not in image_names: 1651 img = image_utils.load_image( 1652 REINFORCED_CONCRETE_MATERIAL['rc_displacement']) 1653 img.name = 'rc_displacement' 1654 img.colorspace_settings.name = 'Non-Color' 1655 1656 if 'di_base_color' not in image_names: 1657 img = image_utils.load_image( 1658 DUCTIL_IRON_MATERIAL['di_base_color']) 1659 img.name = 'di_base_color' 1660 if 'di_roughness' not in image_names: 1661 img = image_utils.load_image( 1662 DUCTIL_IRON_MATERIAL['di_roughness']) 1663 img.name = 'di_roughness' 1664 img.colorspace_settings.name = 'Non-Color' 1665 if 'di_normal' not in image_names: 1666 img = image_utils.load_image( 1667 DUCTIL_IRON_MATERIAL['di_normal']) 1668 img.name = 'di_normal' 1669 img.colorspace_settings.name = 'Non-Color' 1670 if 'di_displacement' not in image_names: 1671 img = image_utils.load_image( 1672 DUCTIL_IRON_MATERIAL['di_displacement']) 1673 img.name = 'di_displacement' 1674 img.colorspace_settings.name = 'Non-Color' 1675 1676 if 'pe_base_color' not in image_names: 1677 img = image_utils.load_image( 1678 POLYETHYLENE_MATERIAL['pe_base_color']) 1679 img.name = 'pe_base_color' 1680 if 'pe_roughness' not in image_names: 1681 img = image_utils.load_image( 1682 POLYETHYLENE_MATERIAL['pe_roughness']) 1683 img.name = 'pe_roughness' 1684 img.colorspace_settings.name = 'Non-Color' 1685 if 'pe_normal' not in image_names: 1686 img = image_utils.load_image( 1687 POLYETHYLENE_MATERIAL['pe_normal']) 1688 img.name = 'pe_normal' 1689 img.colorspace_settings.name = 'Non-Color' 1690 if 'pe_displacement' not in image_names: 1691 img = image_utils.load_image( 1692 POLYETHYLENE_MATERIAL['pe_displacement']) 1693 100 img.name = 'pe_displacement' 1694 img.colorspace_settings.name = 'Non-Color' 1695 1696 if 'pvc_base_color' not in image_names: 1697 img = image_utils.load_image( 1698 POLYVINYL_CHLORIDE_MATERIAL['pvc_base_color']) 1699 img.name = 'pvc_base_color' 1700 if 'pvc_roughness' not in image_names: 1701 img = image_utils.load_image( 1702 POLYVINYL_CHLORIDE_MATERIAL['pvc_roughness']) 1703 img.name = 'pvc_roughness' 1704 img.colorspace_settings.name = 'Non-Color' 1705 if 'pvc_normal' not in image_names: 1706 img = image_utils.load_image( 1707 POLYVINYL_CHLORIDE_MATERIAL['pvc_normal']) 1708 img.name = 'pvc_normal' 1709 img.colorspace_settings.name = 'Non-Color' 1710 if 'pvc_displacement' not in image_names: 1711 img = image_utils.load_image( 1712 POLYVINYL_CHLORIDE_MATERIAL['pvc_displacement']) 1713 img.name = 'pvc_displacement' 1714 img.colorspace_settings.name = 'Non-Color' 1715 1716 # Specify material node parameters. 1717 mat_nodes['Image Texture'].image = bpy.data.images['rc_base_color'] 1718 mat_nodes['Image Texture.001'].image = bpy.data.images['rc_roughness'] 1719 mat_nodes['Image Texture.002'].image = bpy.data.images['rc_normal'] 1720 mat_nodes['Image Texture.003'].image = bpy.data.images['rc_displacement'] 1721 1722 # Create material links. 1723 mat.node_tree.links.new( 1724 mat_nodes['Texture Coordinate'].outputs['UV'], 1725 mat_nodes['Mapping'].inputs['Vector']) 1726 1727 mat.node_tree.links.new( 1728 mat_nodes['Mapping'].outputs['Vector'], 1729 mat_nodes['Image Texture'].inputs['Vector']) 1730 mat.node_tree.links.new( 1731 mat_nodes['Mapping'].outputs['Vector'], 1732 mat_nodes['Image Texture.001'].inputs['Vector']) 1733 mat.node_tree.links.new( 1734 mat_nodes['Mapping'].outputs['Vector'], 1735 mat_nodes['Image Texture.002'].inputs['Vector']) 1736 mat.node_tree.links.new( 1737 mat_nodes['Mapping'].outputs['Vector'], 1738 mat_nodes['Image Texture.003'].inputs['Vector']) 1739 1740 mat.node_tree.links.new( 1741 mat_nodes['Image Texture.002'].outputs['Color'], 1742 mat_nodes['Normal Map'].inputs['Color']) 1743 1744 mat.node_tree.links.new( 1745 mat_nodes['Image Texture'].outputs['Color'], 1746 mat_nodes['Principled BSDF'].inputs['Base Color']) 1747 mat.node_tree.links.new( 1748 mat_nodes['Image Texture.001'].outputs['Color'], 1749 mat_nodes['Principled BSDF'].inputs['Roughness']) 1750 mat.node_tree.links.new( 1751 mat_nodes['Normal Map'].outputs['Normal'], 1752 101 mat_nodes['Principled BSDF'].inputs['Normal']) 1753 mat.node_tree.links.new( 1754 mat_nodes['Image Texture.003'].outputs['Color'], 1755 mat_nodes['Displacement'].inputs['Height']) 1756 1757 mat.node_tree.links.new( 1758 mat_nodes['Principled BSDF'].outputs['BSDF'], 1759 mat_nodes['Material Output'].inputs['Surface']) 1760 mat.node_tree.links.new( 1761 mat_nodes['Displacement'].outputs['Displacement'], 1762 mat_nodes['Material Output'].inputs['Displacement']) 1763 1764 main(context, self) 1765 1766 return {'FINISHED'} 1767 1768 1769 def menu_func(self, context): 1770 self.layout.operator(OBJECT_OT_pipe.bl_idname) 1771 1772 def register(): 1773 bpy.utils.register_class(OBJECT_OT_pipe) 1774 bpy.types.VIEW3D_MT_mesh_add.append(menu_func) 1775 1776 def unregister(): 1777 bpy.utils.unregister_class(OBJECT_OT_pipe) 1778 bpy.types.VIEW3D_MT_mesh_add.remove(menu_func) 1779 1780 1781 if __name__ == '__main__': 1782 register() 1783 102 ANEJO III: Puertos 103 Escollera natural (Generación procedimental) breakwater_rock.py from math import pi as PI 1784 import random 1785 import bpy 1786 1787 1788 class RockMaterial(): 1789 def __init__(self, base_color_filepath, roughness_filepath, normal_filepath, 1790 displacement_filepath): 1791 self.base_color_filepath = base_color_filepath 1792 self.roughness_filepath = roughness_filepath 1793 self.normal_filepath = normal_filepath 1794 self.displacement_filepath = displacement_filepath 1795 1796 1797 class NaturalRockFill(): 1798 def __init__(self, material=None, min_weight=1.0, max_weight=10.0, 1799 density=2.65, y_scale=1.0, z_scale=1.0, levels=4, noise_type='CLOUDS', 1800 noise_basis='ORIGINAL_PERLIN', strength=0.2, seed=0): 1801 self.material = material 1802 self.min_weight = min_weight 1803 self.max_weight = max_weight 1804 self.density = density 1805 self.size_range = (( 1806 self.min_weight / self.density)**(1/3) / 2, 1807 (self.max_weight / self.density)**(1/3) / 2) 1808 self.y_scale = y_scale # Y scale range -> [0.2, 1] 1809 self.z_scale = z_scale # Z scale range -> [0.2, 1] 1810 self.levels = levels 1811 self.noise_type = noise_type 1812 self.noise_basis = noise_basis 1813 self.strength = strength 1814 self.seed = seed 1815 1816 def blendify(self): 1817 # Create collection. 1818 col = bpy.data.collections.new('Breakwater') 1819 bpy.context.scene.collection.children.link(col) 1820 1821 master_col = bpy.context.collection 1822 1823 # Create base cube. 1824 bpy.ops.mesh.primitive_cube_add() 1825 obj = bpy.context.object 1826 obj.name = 'Rock' 1827 col.objects.link(obj) 1828 master_col.objects.unlink(obj) 1829 1830 # Move vertices within ranges. 1831 if self.seed: 1832 random.seed(self.seed) 1833 for i in range(8): 1834 for j in range(3): 1835 obj.data.vertices[i].co[j] *= self.size_range[0] + \ 1836 (self.size_range[1] - self.size_range[0]) * random.random() 1837 1838 # Reposition object in the breakwater. 1839 obj.location = (0.0, 0.0, 0.0) 1840 104 obj.scale = (1.0, self.y_scale, self.z_scale) 1841 obj.rotation_euler = (0.0, 0.0, 0.0) 1842 1843 # Add subdivision surface modifier. 1844 subivision_surface_mod = obj.modifiers.new( 1845 'Subdivision surface', type='SUBSURF') 1846 subivision_surface_mod.levels = self.levels 1847 subivision_surface_mod.render_levels = self.levels 1848 1849 # Create displace texture. 1850 if self.noise_type == 'CLOUDS': 1851 displace_tex = bpy.data.textures.new('Displace texture', type='CLOUDS') 1852 displace_tex.noise_basis = self.noise_basis 1853 if self.levels == 4: 1854 displace_tex.noise_scale = 0.75 + 1.25 * random.random() 1855 elif self.levels == 5: 1856 displace_tex.noise_scale = 1.125 + 0.875 * random.random() 1857 elif self.levels == 6: 1858 displace_tex.noise_scale = 1.5 + 0.5 * random.random() 1859 displace_tex.noise_depth = 10 1860 1861 elif self.noise_type == 'VORONOI': 1862 displace_tex = bpy.data.textures.new('Displace texture', type='VORONOI') 1863 displace_tex.noise_intensity = 0.8 + 0.2 * random.random() 1864 displace_tex.noise_scale = 0.9 + 0.3 * random.random() 1865 displace_tex.weight_1 = 1 1866 displace_tex.weight_2 = 0.5 + 1.5 * random.random() 1867 elif self.noise_type == 'STUCCI': 1868 displace_tex = bpy.data.textures.new('Displace texture', type='STUCCI') 1869 displace_tex.noise_basis = self.noise_basis 1870 1871 # Add displace modifier. 1872 displace_mod = obj.modifiers.new('Displace', type='DISPLACE') 1873 displace_mod.texture = displace_tex 1874 displace_mod.strength = self.strength 1875 1876 # Apply modifiers. 1877 bpy.context.view_layer.objects.active = obj 1878 obj.select_set(True) 1879 bpy.ops.object.modifier_apply(modifier='Subdivision surface') 1880 bpy.ops.object.modifier_apply(modifier='Displace') 1881 1882 # Smooth faces. 1883 bpy.ops.object.shade_smooth() 1884 1885 if self.material: 1886 # Create material. 1887 mat = bpy.data.materials.new(name='Rockfill material') 1888 obj.data.materials.append(mat) 1889 1890 mat.use_nodes = True 1891 mat_nodes = mat.node_tree.nodes 1892 1893 # Create material nodes. 1894 mat_nodes.new('ShaderNodeTexCoord') 1895 mat_nodes.new('ShaderNodeMapping') 1896 mat_nodes.new('ShaderNodeTexImage') # Base Colour. 1897 mat_nodes['Image Texture'].label = 'Base Color' 1898 mat_nodes.new('ShaderNodeTexImage') # Roughness. 1899 mat_nodes['Image Texture.001'].label = 'Roughness' 1900 mat_nodes.new('ShaderNodeTexImage') # Normal. 1901 mat_nodes['Image Texture.002'].label = 'Normal' 1902 mat_nodes.new('ShaderNodeTexImage') # Displacement. 1903 mat_nodes['Image Texture.003'].label = 'Displacement' 1904 mat_nodes.new('ShaderNodeNormalMap') 1905 mat_nodes.new('ShaderNodeDisplacement') 1906 105 1907 # Locate material nodes. 1908 mat_nodes['Texture Coordinate'].location = (-700.0, 125.0) 1909 mat_nodes['Mapping'].location = (-500.0, 175.0) 1910 mat_nodes['Image Texture'].location = (-300.0, 515.0) 1911 mat_nodes['Image Texture.001'].location = (-300.0, 255.0) 1912 mat_nodes['Image Texture.002'].location = (-300.0, -5.0) 1913 mat_nodes['Image Texture.003'].location = (-300.0, -265.0) 1914 mat_nodes['Normal Map'].location = (0.0, 0.0) 1915 mat_nodes['Displacement'].location = (275.0, -300.0) 1916 mat_nodes['Principled BSDF'].location = (225.0, 450.0) 1917 mat_nodes['Material Output'].location = (525.0, 50.0) 1918 1919 # Specify material node parameters. 1920 base_color_tex = bpy.data.images.load(self.material.base_color_filepath) 1921 mat_nodes['Image Texture'].image = base_color_tex 1922 roughness_tex = bpy.data.images.load(self.material.roughness_filepath) 1923 mat_nodes['Image Texture.001'].image = roughness_tex 1924 bpy.data.images[-1].colorspace_settings.name = 'Non-Color' 1925 normal_tex = bpy.data.images.load(self.material.normal_filepath) 1926 mat_nodes['Image Texture.002'].image = normal_tex 1927 bpy.data.images[-1].colorspace_settings.name = 'Non-Color' 1928 displacement_tex = bpy.data.images.load(self.material.displacement_filepath) 1929 mat_nodes['Image Texture.003'].image = displacement_tex 1930 bpy.data.images[-1].colorspace_settings.name = 'Non-Color' 1931 1932 mat_nodes['Principled BSDF'].inputs[5].default_value = 0 # Specular. 1933 1934 # Create material links. 1935 mat.node_tree.links.new( 1936 mat_nodes['Texture Coordinate'].outputs['UV'], 1937 mat_nodes['Mapping'].inputs['Vector']) 1938 1939 mat.node_tree.links.new( 1940 mat_nodes['Mapping'].outputs['Vector'], 1941 mat_nodes['Image Texture'].inputs['Vector']) 1942 mat.node_tree.links.new( 1943 mat_nodes['Mapping'].outputs['Vector'], 1944 mat_nodes['Image Texture.001'].inputs['Vector']) 1945 mat.node_tree.links.new( 1946 mat_nodes['Mapping'].outputs['Vector'], 1947 mat_nodes['Image Texture.002'].inputs['Vector']) 1948 mat.node_tree.links.new( 1949 mat_nodes['Mapping'].outputs['Vector'], 1950 mat_nodes['Image Texture.003'].inputs['Vector']) 1951 1952 mat.node_tree.links.new( 1953 mat_nodes['Image Texture.002'].outputs['Color'], 1954 mat_nodes['Normal Map'].inputs['Color']) 1955 1956 mat.node_tree.links.new( 1957 mat_nodes['Image Texture'].outputs['Color'], 1958 mat_nodes['Principled BSDF'].inputs['Base Color']) 1959 mat.node_tree.links.new( 1960 mat_nodes['Image Texture.001'].outputs['Color'], 1961 mat_nodes['Principled BSDF'].inputs['Roughness']) 1962 mat.node_tree.links.new( 1963 mat_nodes['Normal Map'].outputs['Normal'], 1964 mat_nodes['Principled BSDF'].inputs['Normal']) 1965 mat.node_tree.links.new( 1966 mat_nodes['Image Texture.003'].outputs['Color'], 1967 mat_nodes['Displacement'].inputs['Height']) 1968 1969 mat.node_tree.links.new( 1970 mat_nodes['Principled BSDF'].outputs['BSDF'], 1971 mat_nodes['Material Output'].inputs['Surface']) 1972 112 ( A/2, -D/2, -H/2+B), # 4 2297 ( A/2, D/2, -H/2+B), # 5 2298 (-A/2, D/2, -H/2+B), # 6 2299 (-A/2, -D/2, -H/2+B), # 7 2300 ( D/2, -D/2, -C/2), # 8 2301 ( D/2, D/2, -C/2), # 9 2302 (-D/2, D/2, -C/2), # 10 2303 (-D/2, -D/2, -C/2), # 11 2304 ( D/2, -D/2, C/2), # 12 2305 ( D/2, D/2, C/2), # 13 2306 (-D/2, D/2, C/2), # 14 2307 (-D/2, -D/2, C/2), # 15 2308 ( A/2, -D/2, H/2-B), # 20 2309 ( A/2, D/2, H/2-B), # 21 2310 (-A/2, D/2, H/2-B), # 22 2311 (-A/2, -D/2, H/2-B), # 23 2312 ( A/2, -D/2, H/2), # 16 2313 ( A/2, D/2, H/2), # 17 2314 (-A/2, D/2, H/2), # 18 2315 (-A/2, -D/2, H/2),] # 19 2316 faces = [[ 0, 3, 2, 1], # 0 2317 [ 0, 1, 5, 4], # 1 2318 [ 2, 3, 7, 6], # 2 2319 [ 1, 2, 6, 10, 14, 18, 22, 21, 17, 13, 9, 5], # 3 2320 [ 0, 4, 8, 12, 16, 20, 23, 19, 15, 11, 7, 3], # 4 2321 [ 4, 5, 9, 8], # 5 2322 [ 6, 7, 11, 10], # 6 2323 [ 8, 9, 13, 12], # 7 2324 [12, 13, 17, 16], # 8 2325 [16, 17, 21, 20], # 9 2326 [10, 11, 15, 14], # 10 2327 [14, 15, 19, 18], # 11 2328 [18, 19, 23, 22], # 12 2329 [20, 21, 22, 23]] # 13 2330 2331 create_obj(verts, faces, 'FLAT CROSS AKMON') 2332 2333 def create_hexaleg(tn): 2334 h = HEXALEG[tn] 2335 2336 verts = [( h, h, h), # 0 2337 ( h, -h, h), # 1 2338 ( h, h, 3 * h), # 2 2339 ( h, -h, 3 * h), # 3 2340 (-h, h, 3 * h), # 4 2341 (-h, -h, 3 * h), # 5 2342 ( h, -h, -h), # 6 2343 (-h, h, -h), # 7 2344 (-h, -h, -h), # 8 2345 (-h, -h, -3 * h), # 9 2346 ( h, h, -3 * h), # 10 2347 ( h, -h, -3 * h), # 11 2348 (-h, h, -3 * h), # 12 2349 (-h, h, h), # 13 2350 ( h, h, -h), # 14 2351 (-h, 3 * h, h), # 15 2352 ( h, 3 * h, h), # 16 2353 (-h, 3 * h, -h), # 17 2354 ( h, 3 * h, -h), # 18 2355 ( 3 * h, h, -h), # 19 2356 ( 3 * h, -h, h), # 20 2357 ( 3 * h, -h, -h), # 21 2358 ( 3 * h, h, h), # 22 2359 (-h, -h, h), # 23 2360 ( h, -3 * h, -h), # 24 2361 ( h, -3 * h, h), # 25 2362 113 (-h, -3 * h, -h), # 26 2363 (-h, -3 * h, h), # 27 2364 (-3 * h, -h, h), # 28 2365 (-3 * h, h, -h), # 29 2366 (-3 * h, -h, -h), # 30 2367 (-3 * h, h, h),] # 31 2368 faces = [[23, 8, 30, 28], # 0 2369 [ 0, 1, 23, 13], # 1 2370 [23, 13, 4, 5], # 2 2371 [ 2, 3, 5, 4], # 3 2372 [ 1, 23, 5, 3], # 4 2373 [13, 0, 2, 4], # 5 2374 [ 0, 1, 3, 2], # 6 2375 [ 7, 8, 9, 12], # 7 2376 [ 8, 6, 11, 9], # 8 2377 [12, 9, 11, 10], # 9 2378 [ 6, 14, 10, 11], # 10 2379 [14, 7, 12, 10], # 11 2380 [ 7, 13, 31, 29], # 12 2381 [14, 0, 16, 18], # 13 2382 [ 0, 13, 15, 16], # 14 2383 [17, 18, 16, 15], # 15 2384 [ 7, 14, 18, 17], # 16 2385 [13, 7, 17, 15], # 17 2386 [14, 6, 21, 19], # 18 2387 [ 6, 1, 20, 21], # 19 2388 [19, 21, 20, 22], # 20 2389 [ 0, 14, 19, 22], # 21 2390 [ 1, 0, 22, 20], # 22 2391 [ 1, 6, 24, 25], # 23 2392 [ 6, 8, 26, 24], # 24 2393 [24, 26, 27, 25], # 25 2394 [ 8, 23, 27, 26], # 26 2395 [23, 1, 25, 27], # 27 2396 [30, 29, 31, 28], # 28 2397 [13, 23, 28, 31], # 29 2398 [ 8, 7, 29, 1]] # 30 2399 2400 create_obj(verts, faces, 'HEXALEG') 2401 2402 def create_tripod(tn): 2403 h = TRIPOD[tn] 2404 2405 verts = [(h, 0, 0), # 0 2406 (h, h/2, 0), # 1 2407 (h/2, h/2, 0), # 2 2408 (h/2, h, 0), # 3 2409 (0, h, 0), # 4 2410 (0, 0, 0), # 5 2411 (h, 0, h/2), # 6 2412 (h, h/2, h/2), # 7 2413 (h/2, h/2, h/2), # 8 2414 (h/2, h, h/2), # 9 2415 (0, h, h/2), # 10 2416 (0, h/2, h/2), # 11 2417 (h/2, 0, h/2), # 12 2418 (h/2, 0, h), # 13 2419 (h/2, h/2, h), # 14 2420 (0, h/2, h), # 15 2421 (0, 0, h),] # 16 2422 faces = [[0, 5, 4, 3, 2, 1], # 0 2423 [0, 6, 12, 13, 16, 5], # 1 2424 [4, 5, 16, 15, 11, 10], # 2 2425 [0, 1, 7, 6], # 3 2426 [3, 4, 10, 9], # 4 2427 [13, 14, 15, 16], # 5 2428 114 [6, 7, 8, 12], # 6 2429 [12, 8, 14, 13], # 7 2430 [9, 10, 11, 8], # 8 2431 [8, 11, 15, 14], # 9 2432 [1, 2, 8, 7], # 10 2433 [2, 3, 9, 8]] # 11 2434 2435 create_obj(verts, faces, 'TRIPOD') 2436 2437 def create_cube(tn): 2438 h = CUBE[tn] 2439 2440 verts = [(-h, -h, -h), # 0 2441 (-h, -h, h), # 1 2442 (-h, h, -h), # 2 2443 (-h, h, h), # 3 2444 ( h, -h, -h), # 4 2445 ( h, -h, h), # 5 2446 ( h, h, -h), # 6 2447 ( h, h, h)] # 7 2448 faces = [[ 0, 1, 3, 2], # 0 2449 [ 2, 3, 7, 6], # 1 2450 [ 6, 7, 5, 4], # 2 2451 [ 4, 5, 1, 0], # 3 2452 [ 2, 6, 4, 0], # 4 2453 [ 7, 3, 1, 5]] # 5 2454 2455 create_obj(verts, faces, 'CUBE') 2456 2457 def create_cob(tn): 2458 h = COB[tn][0] 2459 m = COB[tn][1] 2460 2461 verts = [( h/2, -h/2, -h/2), # 0 2462 ( h/2, h/2, -h/2), # 1 2463 (-h/2, h/2, -h/2), # 2 2464 (-h/2, -h/2, -h/2), # 3 2465 ( h/2, -h/2, h/2), # 4 2466 ( h/2, h/2, h/2), # 5 2467 (-h/2, h/2, h/2), # 6 2468 (-h/2, -h/2, h/2), # 7 2469 ( m/2, -m/2, -m/2), # 8 2470 ( m/2, m/2, -m/2), # 9 2471 (-m/2, m/2, -m/2), # 10 2472 (-m/2, -m/2, -m/2), # 11 2473 ( m/2, -m/2, m/2), # 12 2474 ( m/2, m/2, m/2), # 13 2475 (-m/2, m/2, m/2), # 14 2476 (-m/2, -m/2, m/2), # 15 2477 ( h/2, -m/2, -m/2), # 16 2478 ( h/2, m/2, -m/2), # 17 2479 (-h/2, m/2, -m/2), # 18 2480 (-h/2, -m/2, -m/2), # 19 2481 ( h/2, -m/2, m/2), # 20 2482 ( h/2, m/2, m/2), # 21 2483 (-h/2, m/2, m/2), # 22 2484 (-h/2, -m/2, m/2), # 23 2485 ( m/2, -h/2, -m/2), # 24 2486 ( m/2, h/2, -m/2), # 25 2487 (-m/2, h/2, -m/2), # 26 2488 (-m/2, -h/2, -m/2), # 27 2489 ( m/2, -h/2, m/2), # 28 2490 ( m/2, h/2, m/2), # 29 2491 (-m/2, h/2, m/2), # 30 2492 (-m/2, -h/2, m/2), # 31 2493 ( m/2, -m/2, -h/2), # 32 2494 115 ( m/2, m/2, -h/2), # 33 2495 (-m/2, m/2, -h/2), # 34 2496 (-m/2, -m/2, -h/2), # 35 2497 ( m/2, -m/2, h/2), # 36 2498 ( m/2, m/2, h/2), # 37 2499 (-m/2, m/2, h/2), # 38 2500 (-m/2, -m/2, h/2),] # 39 2501 faces = [[0, 1, 17, 16], # 0 2502 [1, 2, 26, 25], # 1 2503 [2, 3, 19, 18], # 2 2504 [3, 0, 24, 27], # 3 2505 [1, 0, 32, 33], #s 4 2506 [2, 1, 33, 34], # 5 2507 [3, 2, 34, 35], # 6 2508 [0, 3, 35, 32], # 7 2509 [4, 5, 37, 36], # 8 2510 [5, 6, 38, 37], # 9 2511 [6, 7, 39, 38], # 10 2512 [7, 4, 36, 39], # 11 2513 [5, 4, 20, 21], # 12 2514 [6, 5, 29, 30], # 13 2515 [7, 6, 22, 23], # 14 2516 [4, 7, 31, 28], # 15 2517 [9, 8, 16, 17], # 16 2518 [10, 9, 25, 26], # 17 2519 [11, 10, 18, 19], # 18 2520 [8, 11, 27, 24], # 19 2521 [8, 9, 33, 32], # 20 2522 [9, 10, 34, 33], # 21 2523 [10, 11, 35, 34], # 22 2524 [11, 8, 32, 35], # 23 2525 [13, 12, 36, 37], # 24 2526 [14, 13, 37, 38], # 25 2527 [15, 14, 38, 39], # 26 2528 [12, 15, 39, 36], # 27 2529 [12, 13, 21, 20], # 28 2530 [13, 14, 30, 29], # 29 2531 [14, 15, 23, 22], # 30 2532 [15, 12, 28, 31], # 31 2533 [8, 12, 20, 16], # 32 2534 [12, 8, 24, 28], # 33 2535 [13, 9, 17, 21], # 34 2536 [9, 13, 29, 25], # 35 2537 [10, 14, 22, 18], # 36 2538 [14, 10, 26, 30], # 37 2539 [15, 11, 19, 23], # 38 2540 [11, 15, 31, 27], # 39 2541 [0, 16, 20, 4], # 40 2542 [1, 5, 21, 17], # 41 2543 [1, 25, 29, 5], # 42 2544 [2, 6, 30, 26], # 43 2545 [2, 18, 22, 6], # 44 2546 [3, 7, 23, 19], # 45 2547 [3, 27, 31, 7], # 46 2548 [0, 4, 28, 24]] # 47 2549 create_obj(verts, faces, 'COB') 2550 2551 def create_dolos(tn): 2552 h = DOLOS[tn][0] 2553 a = DOLOS[tn][1] 2554 b = DOLOS[tn][2] 2555 c = DOLOS[tn][3] 2556 d = DOLOS[tn][4] 2557 f = DOLOS[tn][5] 2558 I = DOLOS[tn][6] 2559 2560 116 m = a / (2.4142135623730951) 2561 r = m / 1.4142135623730951 2562 mb = b / (2.4142135623730951) 2563 rb = mb / 1.4142135623730951 2564 mc = c / (2.4142135623730951) 2565 rc = mc / 1.4142135623730951 2566 2567 __t = 0.01 2568 2569 verts = [( h/2-a/2+c/2, mc/2, -h/2), # 0 2570 ( h/2-a/2+c/2, -mc/2, -h/2), # 1 2571 ( h/2-a/2+c/2-rc, -mc/2-rc, -h/2), # 2 2572 ( h/2-a/2+c/2-rc-mc, -mc/2-rc, -h/2), # 3 2573 ( h/2-a/2-c/2, -mc/2, -h/2), # 4 2574 ( h/2-a/2-c/2, mc/2, -h/2), # 5 2575 ( h/2-a/2+c/2-rc-mc, mc/2+rc, -h/2), # 6 2576 ( h/2-a/2+c/2-rc, mc/2+rc, -h/2), # 7 2577 ( h/2-a/2+b/2, mb/2, -h/2+I), # 8 2578 ( h/2-a/2+b/2, -mb/2, -h/2+I), # 9 2579 ( h/2-a/2+b/2-rb, -mb/2-rb, -h/2+I), # 10 2580 ( h/2-a/2+b/2-rb-mb, -mb/2-rb, -h/2+I), # 11 2581 ( h/2-a/2-b/2, -mb/2, -h/2+I), # 12 2582 ( h/2-a/2-b/2, mb/2, -h/2+I), # 13 2583 ( h/2-a/2+b/2-rb-mb, mb/2+rb, -h/2+I), # 14 2584 ( h/2-a/2+b/2-rb, mb/2+rb, -h/2+I), # 15 2585 ( h/2, m/2, -m/2), # 16 2586 ( h/2, -m/2, -m/2), # 17 2587 ( h/2-r, -m/2-r, -m/2), # 18 2588 ( h/2-r-m, -m/2-r, -m/2), # 19 2589 ( h/2-d, -m/2+__t, -a/2-f), # 20 2590 ( h/2-d, m/2-__t, -a/2-f), # 21 2591 ( h/2-r-m, a/2, -m/2), # 22 2592 ( h/2-r, a/2, -m/2), # 23 2593 ( h/2, m/2, m/2), # 24 2594 ( h/2, -m/2, m/2), # 25 2595 ( h/2-r, -m/2-r, m/2), # 26 2596 ( h/2-r-m, -m/2-r, m/2), # 27 2597 ( h/2-d, -m/2+__t, a/2+f), # 28 2598 ( h/2-d, m/2-__t, a/2+f), # 29 2599 ( h/2-r-m, a/2, m/2), # 30 2600 ( h/2-r, a/2, m/2), # 31 2601 ( h/2-a/2+b/2, mb/2, h/2-I), # 32 2602 ( h/2-a/2+b/2, -mb/2, h/2-I), # 33 2603 ( h/2-a/2+b/2-rb, -mb/2-rb, h/2-I), # 34 2604 ( h/2-a/2+b/2-rb-mb, -mb/2-rb, h/2-I), # 35 2605 ( h/2-a/2-b/2, -mb/2, h/2-I), # 36 2606 ( h/2-a/2-b/2, mb/2, h/2-I), # 37 2607 ( h/2-a/2+b/2-rb-mb, mb/2+rb, h/2-I), # 38 2608 ( h/2-a/2+b/2-rb, mb/2+rb, h/2-I), # 39 2609 ( h/2-a/2+c/2, mc/2, h/2), # 40 2610 ( h/2-a/2+c/2, -mc/2, h/2), # 41 2611 ( h/2-a/2+c/2-rc, -mc/2-rc, h/2), # 42 2612 ( h/2-a/2+c/2-rc-mc, -mc/2-rc, h/2), # 43 2613 ( h/2-a/2-c/2, -mc/2, h/2), # 44 2614 ( h/2-a/2-c/2, mc/2, h/2), # 45 2615 ( h/2-a/2+c/2-rc-mc, mc/2+rc, h/2), # 46 2616 ( h/2-a/2+c/2-rc, mc/2+rc, h/2), # 47 2617 (-h/2+a/2+c/2, h/2, mc/2), # 48 2618 (-h/2+a/2+c/2, h/2, -mc/2), # 49 2619 (-h/2+a/2-c/2+rc+mc, h/2, -mc/2-rc), # 50 2620 (-h/2+a/2-c/2+rc, h/2, -mc/2-rc), # 51 2621 (-h/2+a/2-c/2, h/2, -mc/2), # 52 2622 (-h/2+a/2-c/2, h/2, mc/2), # 53 2623 (-h/2+a/2-c/2+rc, h/2, mc/2+rc), # 54 2624 (-h/2+a/2-c/2+rc+mc, h/2, mc/2+rc), # 55 2625 (-h/2+a/2+b/2, h/2-I, mb/2), # 56 2626 117 (-h/2+a/2+b/2, h/2-I, -mb/2), # 57 2627 (-h/2+a/2-b/2+rb+mb, h/2-I, -mb/2-rb), # 58 2628 (-h/2+a/2-b/2+rb, h/2-I, -mb/2-rb), # 59 2629 (-h/2+a/2-b/2, h/2-I, -mb/2), # 60 2630 (-h/2+a/2-b/2, h/2-I, mb/2), # 61 2631 (-h/2+a/2-b/2+rb, h/2-I, mb/2+rb), # 62 2632 (-h/2+a/2-b/2+rb+mb, h/2-I, mb/2+rb), # 63 2633 (-h/2+d, a/2+f, m/2-__t), # 64 2634 (-h/2+d, a/2+f, -m/2+__t), # 65 2635 (-h/2+r, m/2, -m/2-r), # 66 2636 (-h/2+r+m, m/2, -m/2-r), # 67 2637 (-h/2, m/2, -m/2), # 68 2638 (-h/2, m/2, m/2), # 69 2639 (-h/2+r+m, m/2, a/2), # 70 2640 (-h/2+r, m/2, a/2), # 71 2641 (-h/2+d, -a/2-f, m/2-__t), # 72 2642 (-h/2+d, -a/2-f, -m/2+__t), # 73 2643 (-h/2+r, -m/2, -m/2-r), # 74 2644 (-h/2+r+m, -m/2, -m/2-r), # 75 2645 (-h/2, -m/2, -m/2), # 76 2646 (-h/2, -m/2, m/2), # 77 2647 (-h/2+r+m, -m/2, a/2), # 78 2648 (-h/2+r, -m/2, a/2), # 79 2649 (-h/2+a/2+b/2, -h/2+I, mb/2), # 80 2650 (-h/2+a/2+b/2, -h/2+I, -mb/2), # 81 2651 (-h/2+a/2-b/2+rb+mb, -h/2+I, -mb/2-rb), # 82 2652 (-h/2+a/2-b/2+rb, -h/2+I, -mb/2-rb), # 83 2653 (-h/2+a/2-b/2, -h/2+I, -mb/2), # 84 2654 (-h/2+a/2-b/2, -h/2+I, mb/2), # 85 2655 (-h/2+a/2-b/2+rb, -h/2+I, mb/2+rb), # 86 2656 (-h/2+a/2-b/2+rb+mb, -h/2+I, mb/2+rb), # 87 2657 (-h/2+a/2+c/2, -h/2, mc/2), # 88 2658 (-h/2+a/2+c/2, -h/2, -mc/2), # 89 2659 (-h/2+a/2-c/2+rc+mc, -h/2, -mc/2-rc), # 90 2660 (-h/2+a/2-c/2+rc, -h/2, -mc/2-rc), # 91 2661 (-h/2+a/2-c/2, -h/2, -mc/2), # 92 2662 (-h/2+a/2-c/2, -h/2, mc/2), # 93 2663 (-h/2+a/2-c/2+rc, -h/2, mc/2+rc), # 94 2664 (-h/2+a/2-c/2+rc+mc, -h/2, mc/2+rc), # 95 2665 ( h/2-d-f, -m/2, -a/2), # 96 2666 ( h/2-d-f, m/2, -a/2), # 97 2667 ( h/2-d-f, -m/2, a/2), # 98 2668 ( h/2-d-f, m/2, a/2), # 99 2669 (-h/2+d+f, a/2, m/2), # 100 2670 (-h/2+d+f, a/2, -m/2), # 101 2671 (-h/2+d+f, -a/2, m/2), # 102 2672 (-h/2+d+f, -a/2, -m/2),] # 103 2673 faces = [[0, 1, 2, 3, 4, 5, 6, 7], # 0 2674 [0, 8, 9, 1], # 1 2675 [8, 16, 17, 9], # 2 2676 [16, 24, 25, 17], # 3 2677 [24, 32, 33, 25], # 4 2678 [32, 40, 41, 33], # 5 2679 [1, 9, 10, 2], # 6 2680 [9, 17, 18, 10], # 7 2681 [17, 25, 26, 18], # 8 2682 [25, 33, 34, 26], # 9 2683 [33, 41, 42, 34], # 10 2684 [2, 10, 11, 3], # 11 2685 [10, 18, 19, 11], # 12 2686 [18, 26, 27, 19], # 13 2687 [26, 34, 35, 27], # 14 2688 [34, 42, 43, 35], # 15 2689 [3, 11, 12, 4], # 16 2690 [11, 19, 20, 12], # 17 2691 [27, 35, 36, 28], # 18 2692 118 [35, 43, 44, 36], # 19 2693 [4, 12, 13, 5], # 20 2694 [12, 20, 21, 13], # 21 2695 [28, 36, 37, 29], # 22 2696 [36, 44, 45, 37], # 23 2697 [5, 13, 14, 6], # 24 2698 [13, 21, 22, 14], # 25 2699 [29, 37, 38, 30], # 26 2700 [37, 45, 46, 38], # 27 2701 [6, 14, 15, 7], # 28 2702 [14, 22, 23, 15], # 29 2703 [22, 30, 31, 23], # 30 2704 [30, 38, 39, 31], # 31 2705 [38, 46, 47, 39], # 32 2706 [7, 15, 8, 0], # 33 2707 [15, 23, 16, 8], # 34 2708 [23, 31, 24, 16], # 35 2709 [31, 39, 32, 24], # 36 2710 [39, 47, 40, 32], # 37 2711 [47, 46, 45, 44, 43, 42, 41, 40], # 38 2712 [96, 75, 67, 97], # 39 2713 [97, 67, 101, 22], # 40 2714 [22, 101, 100, 30], # 41 2715 [30, 100, 71, 99], # 42 2716 [99, 70, 78, 98], # 43 2717 [98, 78, 102, 27], # 44 2718 [27, 102, 103, 19], # 45 2719 [19, 103, 75, 96], # 46 2720 [19, 96, 20], # 47 2721 [20, 96, 97, 21], # 48 2722 [21, 97, 22], # 49 2723 [30, 99, 29], # 50 2724 [29, 99, 98, 28], # 51 2725 [28, 98, 27], # 52 2726 [67, 65, 101], # 53 2727 [65, 64, 100, 101], # 54 2728 [64, 70, 100], # 55 2729 [75, 103, 73], # 56 2730 [73, 103, 102, 72], # 57 2731 [72, 102, 78], # 58 2732 [48, 49, 50, 51, 52, 53, 54, 55], # 59 2733 [48, 56, 57, 49], # 60 2734 [56, 64, 65, 57], # 61 2735 [72, 80, 81, 73], # 62 2736 [80, 88, 89, 81], # 63 2737 [49, 57, 58, 50], # 64 2738 [57, 65, 67, 58], # 65 2739 [73, 81, 82, 75], # 66 2740 [81, 89, 90, 82], # 67 2741 [50, 58, 59, 51], # 68 2742 [58, 67, 66, 59], # 69 2743 [66, 67, 75, 74], # 70 2744 [75, 82, 83, 74], # 71 2745 [82, 90, 91, 83], # 72 2746 [51, 59, 60, 52], # 73 2747 [59, 66, 68, 60], # 74 2748 [66, 74, 76, 68], # 75 2749 [74, 83, 84, 76], # 76 2750 [83, 91, 92, 84], # 77 2751 [52, 60, 61, 53], # 78 2752 [60, 68, 69, 61], # 79 2753 [68, 76, 77, 69], # 80 2754 [76, 84, 85, 77], # 81 2755 [84, 92, 93, 85], # 82 2756 [53, 61, 62, 54], # 83 2757 [61, 69, 71, 62], # 84 2758 119 [69, 77, 79, 71], # 85 2759 [77, 85, 86, 79], # 86 2760 [85, 93, 94, 86], # 87 2761 [55, 54, 62, 63], # 88 2762 [63, 62, 71, 70], # 89 2763 [70, 71, 79, 78], # 90 2764 [78, 79, 86, 87], # 91 2765 [87, 86, 94, 95], # 92 2766 [55, 63, 56, 48], # 93 2767 [63, 70, 64, 56], # 94 2768 [78, 87, 80, 72], # 95 2769 [87, 95, 88, 80], # 96 2770 [95, 94, 93, 92, 91, 90, 89, 88]] # 97 2771 create_obj(verts, faces, 'DOLOS') 2772 2773 2774 if __name__ == '__main__': 2775 pass 2776