Repository navigation
Log‐FURY
En esta página voy a ir escribiendo notas y experimentos sobre las simulaciones físicas y de robots
Es es un tema que utilicé en mi tesis para simular robots modulares, y llevarlos a la práctica. Obtuve unos resultados muy interesantes. Por aquel entonces utilicé el motor físico ODE y lenguaje C. Escribí un tutorial para organizar todo este conocimiento aprendido:
- Tutorial: ODE y Robots modulares: Simulación de robots ápodos modulares con ODE
En su momento reharé este tutorial, pero en lenguaje python y utilizando herramientas más modernas. Lo primero es familiarizarme con estas nuevas herramientas. Los progresos los iré poniendo en este log, que en el futuro cristalizará en un tutorial
Como simulador físico voy a probar el pybullet
Este es el simulador empleado en la asignatura de modelado y simulación, impartida por mi compañero Roberto Calvo. En este repositorio tiene unos ejemplos muy buenos para comenzar:
si bien pybullet tiene integrado ya una GUI donde se renderizan las simulaciones en 3D, voy a realizar el renderizado, inicialmente, utilizando Fury: Free Unified Rendering in Python. Es un módulo python para renderizar imágnes científicas
La página con los tutoriales está caida hoy (qué mala suerte...). Pero todos los ejemplos están su repositorio de github. Así que los voy a ir poniendo en marcha desde ahí
Los tutoriales se generan dinámicamente a partir de la info del repo. En este archivo se pueden ver las secciones del tutorial y los nombres de los diferentes ejemplos: https://github.com/fury-gl/fury/blob/master/docs/examples/_valid_examples.toml
Lo primero es preparar el entorno. En el repositorio de este Log se ha creado un entorno virtual de python y se han instalado los paquetes:
pip install numpy
pip install furyEstoy usando VSCODE 1.99.0:

El ejemplo "hola mundo" mínimo, que renderiza un cubo rojo unitario (1x1x1) en la pantalla es el siguiente:
import numpy as np
import fury
# --------- Constantes
ORIGEN = np.zeros([1, 3])
# -- Crear un cubo en el origen. Por defecto es un cubo de 1x1x1
# -- y el color es rojo
cubo = fury.actor.cube(ORIGEN)
# -- Creamos una escena nueva vacia
# -- y añadimos el cubo
scene = fury.window.Scene()
scene.add(cubo)
# -- Mostrar la ventana principal
# -- Por defecto tiene un tamaño de 300x300
fury.window.show(scene)Si lo tenemos todo instalado correcta se ejecuta muy fácilmente pinchando en el botón de play de la parte superior derecha. En la parte inferior derecha observamos que estamos trabajando en el entorno virtual de python

Al ejecutar el programa se nos abre una ventana pequeña, de 300x300 con el cubo rojo:

Por la posición inicial de la cámara, este cubo se muestra en 2D. Pero utilizando el ratón y los controles de Blender podemos rotar la vista, desplazarla y hacer zoom
- Rueda del ratón: zoom dentro/fuera
- Botón izquierdo + arrastrar: rotar la cámara
- Botón central + arrastrar: Mover la cámara

En este ejemplo añadimos 2 cubos a la escena. Uno es un cubo blanco situado en el origen. El otro es un cubo rojo del mismo tamaño situado a 2 unidades hacia el eje x. Es decir, que está en la posición (2, 0, 0). Lo que quiero determinar es hacia donde está cada eje en fury
Además usamos una pantalla más grande, de 600x600
Este es el código:
"""
Dibujar dos cubos iguales, uno es blanco y el otro rojo, separados una
distancia de 2 unidades en el eje X.
"""
import numpy as np
import fury
# ------------------ Constantes
ORIGEN = np.zeros([1, 3])
# -- Colores
BLANCO = np.array([1, 1, 1])
RED = np.array([1, 0, 0])
BLUE = np.array([0, 0, 1])
# -- Tamaño de la ventana
PANTALLA = (600, 600)
# ------------------- Construir los objetos a visualizar
# --- Primer cubo
cubo1 = fury.actor.cube(ORIGEN, colors=BLANCO)
# -- Crear el segundo cubo
cubo2 = fury.actor.cube(ORIGEN, colors=RED)
cubo2.SetPosition(2, 0, 0) # -- Cambiamos la posicion del cubo2
# ----------------- Crear la escena con los elementos
scene = fury.window.Scene()
scene.add(cubo1)
scene.add(cubo2)
# -- Mostrar la ventana principal
# -- Por defecto tiene un tamaño de 300x300
fury.window.show(scene, size=PANTALLA)Este es el resultado nada más ejecutarse el programa. Vemos que el cubo rojo está situado a la derecha del origen. Ya sabemos hacia dónde apunta el eje x

Y aquí lo vemos cambiando el ángulo de la cámara:

En este ejemplo quiero caracterizar todos los ejes coordenadas. Colocamos 4 cubos iguales en tamaño, uno en el origen y 3 en el resto de ejes, a 2 unidades de distancia del origen, con los colores Rojo (X), Verde(Y) y Azul(Z) (Mismos colores que se usan en Freecad)
Este es el código
"""
Dibujar cuatro cubos iguales:
* Cubo 1: (BLANCO) en el origen
* Cubo 2: (ROJO) en el eje X (2,0,0)
* Cubo 3: (VERDE) en el eje Y (0,2,0)
* Cubo 4: (AZUL) en el eje Z (0,0,2)
"""
import numpy as np
from fury import actor, window
# ------------------ Constantes
ORIGEN = np.zeros([1, 3])
# -- Colores
WHITE = np.array([1, 1, 1])
RED = np.array([1, 0, 0])
GREEN = np.array([0, 1, 0])
BLUE = np.array([0, 0, 1])
# -- Tamaño de la ventana
PANTALLA = (600, 600)
# ------------------- Construir los objetos a visualizar
# --- Crear los cubos
cubo1 = actor.cube(ORIGEN, colors=WHITE)
cubo2 = actor.cube(ORIGEN, colors=RED)
cubo3 = actor.cube(ORIGEN, colors=GREEN)
cubo4 = actor.cube(ORIGEN, colors=BLUE)
# --- Posicionar los cubos
cubo2.SetPosition(2, 0, 0) # Cubo en el eje X
cubo3.SetPosition(0, 2, 0) # Cubo en el eje Y
cubo4.SetPosition(0, 0, 2) # Cubo en el eje Z
# ----------------- Crear la escena con los elementos
scene = window.Scene()
scene.add(cubo1)
scene.add(cubo2)
scene.add(cubo3)
scene.add(cubo4)
# ----------------- Crear la ventana
# -- Mostrar la ventana principal
# -- Por defecto tiene un tamaño de 300x300
window.show(scene, size=PANTALLA)Esta es la imagen que se obtiene al ejecutarlo:

Y esta es la misma pero rotada para verla en 3D:

Este es el mismo ejemplo que el 3: se dibujan los 4 mismos cubos, pero como un único actor. Se declara un array con los centros de los cuatro cubos, y otro con los colores
"""
Dibujar cuatro cubos iguales:
* Cubo 1: (BLANCO) en el origen
* Cubo 2: (ROJO) en el eje X (2,0,0)
* Cubo 3: (VERDE) en el eje Y (0,2,0)
* Cubo 4: (AZUL) en el eje Z (0,0,2)
"""
import numpy as np
from fury import actor, window
# ------------------ Constantes
ORIGEN = np.array([0, 0, 0])
POS_EJEX = np.array([2, 0, 0])
POS_EJEY = np.array([0, 2, 0])
POS_EJEZ = np.array([0, 0, 2])
# -- Colores
WHITE = np.array([1, 1, 1])
RED = np.array([1, 0, 0])
GREEN = np.array([0, 1, 0])
BLUE = np.array([0, 0, 1])
# -- Tamaño de la ventana
PANTALLA = (600, 600)
# ---- Construir los objetos a visualizar
# -- Array2 con las posiciones de los cubos y sus colores
# Cubo 1 Cubo 2 Cubo 3 Cubo 4
centers = np.array([ORIGEN, POS_EJEX, POS_EJEY, POS_EJEZ])
colors = np.array([WHITE, RED, GREEN, BLUE])
# --- Crear los cubos
cubos = actor.cube(centers, colors=colors)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(cubos)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)El resultado es exactamente el mismo que el ejemplo 3
Vamos a dibujar una flecha roja en el sentido del eje X. Dibujar flechas es importante porque nos permite visualizar vectores
"""
Dibujar una flecha roja apuntando hacia el eje X (sentido positivo)
"""
import numpy as np
from fury import actor, window
# ------------------ Constantes
ORIGEN = np.array([0, 0, 0])
EJE_X = np.array([1, 0, 0])
# -- Colores
RED = np.array([1, 0, 0])
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ---- Construir los objetos a visualizar
centers = np.array([ORIGEN])
colors = np.array([RED])
directions = np.array([EJE_X])
# --- Crear la flecha
flecha = actor.arrow(centers, directions, colors)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(flecha)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)Este es el resultado:


Dibujar un eje coordenado a partir de 3 flechas. Se sigue la notación de Freecad: flecha roja es el eje x, flecha verde el eje y, flecha azul el eje z
"""
Dibujar tres flechas, en los tres ejes, de diferentes colores
"""
import numpy as np
from fury import actor, window
# ------------------ Constantes
ORIGEN = np.array([0, 0, 0])
EJE_X = np.array([1, 0, 0])
EJE_Y = np.array([0, 1, 0])
EJE_Z = np.array([0, 0, 1])
# -- Colores
RED = np.array([1, 0, 0])
GREEN = np.array([0, 1, 0])
BLUE = np.array([0, 0, 1])
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ---- Construir las flechas
centers = np.array([ORIGEN, ORIGEN, ORIGEN])
colors = np.array([RED, GREEN, BLUE])
directions = np.array([EJE_X, EJE_Y, EJE_Z])
# --- Crear las flechas
flecha = actor.arrow(centers, directions, colors)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(flecha)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)Este es el resultado:


En el ejemplo anterior ya hemos creado un sistema coordenado en el origen, a partir de 3 flechas. Pero lo podemos hacer de manera más fácil utilizando el método axes(). No hace falta pasar ningún parámetro. Mete el eje coordenado en el origen por defecto, y con los colores estándard
"""
Dibujar un sistema de coordenadas en el origen
"""
from fury import actor, window
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# --- Crear el sistema coordenado
ejes = actor.axes()
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)El resultado es el mismo que antes:

Las esferas son muy importantes porque nos permiten representar puntos en el espacio. En este ejemplo dibujamos 2 esferas del mismo tamaño. Una en el origen de coordenadas y otra en la posición (1,1,1). Además colocamos un sistema de ejes como referencia
"""
Dibujar dos esferas, una blanca en el origen y otra amarilla
en la posición (1, 1, 1)
"""
import numpy as np
from fury import actor, window
# -- Constantes
ORIGEN = np.array([0, 0, 0])
PUNTO = np.array([1, 1, 1])
WHITE = np.array([1, 1, 1])
YELLOW = np.array([1, 1, 0])
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# -- Crear las esferas
centers = np.array([ORIGEN, PUNTO])
colors = np.array([WHITE, YELLOW])
esfera1 = actor.sphere(centers, colors, radii=0.1)
# --- Crear el sistema coordenado
ejes = actor.axes()
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(esfera1)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)Este es el resultado:


En este ejemplo se posiciona la cámara automáticamente para ver el cubo con perspectiva. El cubo es de color blanco transparente. Se colocan también unos ejes coordenados para ver mejor la horientación del cubo
"""
Poner un Cubo en el origen de color blanco con transparencia
También un sistema de coordenadas
Posicionar la cámara
"""
import numpy as np
from fury import actor, window
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
# -- Color blanco con transparencia
WHITE_TRANS = np.array([1, 1, 1, 0.8])
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
centers = np.array([ORIGEN])
colors = np.array([WHITE_TRANS])
# --- Crear los cubos
cubos = actor.cube(centers, colors=colors)
# --- Crear el sistema coordenado
ejes = actor.axes()
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(cubos)
# --- Cambiar la posición de la cámara
scene.GetActiveCamera().SetPosition(-50, 30, 80)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)Resultado:

En este ejemplo se usa una caja (Box) como suelo. En su cara superior se sitúa el sistema de coordenadas
"""
Dibujar un suelo en 3D con un sistema de coordenadas
"""
import numpy as np
from fury import actor, window
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
# -- Color blanco con transparencia
GREY = np.array([0.5, 0.5, 0.5])
# -- Dimensiones del suelo
GROUND_WIDTH = 5
GROUND_DEPTH = 5
GROUND_HEIGHT = 0.05
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
centers = np.array([ORIGEN])
colors = np.array([GREY])
size = np.array([GROUND_WIDTH, GROUND_HEIGHT, GROUND_DEPTH])
# --- Crear el suelo
suelo = actor.cube(centers, colors=colors, scales=size)
suelo.SetPosition(0, -GROUND_HEIGHT/2, 0)
# --- Crear el sistema coordenado
ejes = actor.axes()
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(suelo)
# --- Cambiar la posición de la cámara
scene.GetActiveCamera().SetPosition(-50, 30, 80)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)Resultado:

Este ejemplo es similar al 10: Se muestra un suelo con un sistema coordenado, pero ahora se pone el fondo de color blanco. El sistema de coordenadas se pone en una esquina del suelo
"""
Dibujar un suelo en 3D con un sistema de coordenadas
Cambiar el color del fondo del escenario
"""
import numpy as np
from fury import actor, window
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
# -- Color blanco con transparencia
GREY = np.array([0.6, 0.6, 0.6])
# -- Dimensiones del suelo
GROUND_WIDTH = 5
GROUND_DEPTH = 5
GROUND_HEIGHT = 0.05
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
centers = np.array([ORIGEN])
colors = np.array([GREY])
size = np.array([GROUND_WIDTH, GROUND_HEIGHT, GROUND_DEPTH])
# --- Crear el suelo
suelo = actor.cube(centers, colors=colors, scales=size)
suelo.SetPosition(0, -GROUND_HEIGHT/2, 0)
# --- Crear el sistema coordenado
ejes = actor.axes()
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(suelo)
# --- Cambiar la posición de la cámara
scene.GetActiveCamera().SetPosition(-50, 50, 80)
# --- Cambiar el color del fondo
scene.SetBackground(1, 1, 1)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)Este es el resultado:

Partiendo del ejemplo anterior, colocamos una baldosa amarilla, de 1x1, sobre el suelo
"""
Poner una baldosa amarilla de 1x1 en el suelo
"""
import numpy as np
from fury import actor, window
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
# -- Color blanco con transparencia
GREY = np.array([0.6, 0.6, 0.6])
YELLOW = np.array([1, 1, 0])
# -- Dimensiones del suelo
GROUND_WIDTH = 5
GROUND_DEPTH = 5
GROUND_HEIGHT = 0.05
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
centers = np.array([ORIGEN])
ground_colors = np.array([GREY])
ground_size = np.array([GROUND_WIDTH, GROUND_HEIGHT, GROUND_DEPTH])
# --- Crear el suelo
suelo = actor.cube(centers, colors=ground_colors, scales=ground_size)
suelo.SetPosition(GROUND_WIDTH/2, -GROUND_HEIGHT/2, GROUND_DEPTH/2)
# --- Crear el sistema coordenado
ejes = actor.axes()
# -- Crear la baldosa amarilla de 1x1
baldosa_size = np.array([1, 0.1, 1])
centers = np.array([ORIGEN])
baldosa = actor.cube(centers, colors=YELLOW, scales=baldosa_size)
baldosa.SetPosition(0.5, 0.05, 0.5)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(suelo)
scene.add(baldosa)
# --- Cambiar la posición de la cámara
scene.GetActiveCamera().SetPosition(-50, 50, 80)
# --- Cambiar el color del fondo
scene.SetBackground(1, 1, 1)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)Resultado:

Rotamos la baldosa 45 grados con respecto al suelo (eje Y)
"""
Poner una baldosa amarilla de 1x1 en el suelo
"""
import numpy as np
from fury import actor, window
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
# -- Color blanco con transparencia
GREY = np.array([0.6, 0.6, 0.6])
YELLOW = np.array([1, 1, 0])
# -- Dimensiones del suelo
GROUND_WIDTH = 5
GROUND_DEPTH = 5
GROUND_HEIGHT = 0.05
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
centers = np.array([ORIGEN])
ground_colors = np.array([GREY])
ground_size = np.array([GROUND_WIDTH, GROUND_HEIGHT, GROUND_DEPTH])
# --- Crear el suelo
suelo = actor.cube(centers, colors=ground_colors, scales=ground_size)
suelo.SetPosition(GROUND_WIDTH/2, -GROUND_HEIGHT/2, GROUND_DEPTH/2)
# --- Crear el sistema coordenado
ejes = actor.axes()
# -- Crear la baldosa amarilla de 1x1
baldosa_size = np.array([1, 0.1, 1])
centers = np.array([ORIGEN])
baldosa = actor.cube(centers, colors=YELLOW, scales=baldosa_size)
# -- Cambiar la orientación: Rotar 45 grados paralelamente al suelo (Eje y)
baldosa.SetOrientation(0, 40, 0)
# -- Cambiar la posición
baldosa.SetPosition(0.5, 0.05, 0.5)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(suelo)
scene.add(baldosa)
# --- Cambiar la posición de la cámara
scene.GetActiveCamera().SetPosition(-50, 50, 80)
# --- Cambiar el color del fondo
scene.SetBackground(1, 1, 1)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)Resultado:

En este ejemplo se crea un suelo a base de baldosas iguales (1x1), alternando los colores amarillo y blanco
El número de baldosas está parametrizado
"""
Array NxM de baldosas 1x1 en el suelo
Los colores de las baldosas forman un patrón de ajedrez
de colores blanco y amarillo
"""
import numpy as np
from fury import actor, window
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
# -- Color blanco con transparencia
GREY = np.array([0.6, 0.6, 0.6])
YELLOW = np.array([1, 1, 0])
WHITE = np.array([1, 1, 1])
LIGHTBLUE = np.array([0.5, 0.5, 1])
# -- Numero de baldosas (N x M)
N = 5
M = 5
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
# --- Crear el sistema coordenado
ejes = actor.axes()
# --- Crear las baldosas amarilla de 1x1
baldosa_size = np.array([1, 0.1, 1])
centers = np.array([[i, 0, j] for i in range(N) for j in range(M)])
# -- Color: Patron de ajedrez (Amarillo y blanco)
colors = np.array([WHITE if (i+j) % 2 else YELLOW
for i in range(N)
for j in range(M)])
baldosas = actor.cube(centers, colors=colors, scales=baldosa_size)
# -- Bajar las baldosas para que el origen esté a ras del suelo
baldosas.SetPosition(0, -0.05, 0)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(baldosas)
# --- Cambiar la posición de la cámara
scene.GetActiveCamera().SetPosition(-50, 50, 80)
# --- Cambiar el color del fondo
scene.SetBackground(LIGHTBLUE)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)Resultado:

En este ejemplo se coloca una imagen png con una textura en el suelo. Su tamaño se deja a 1x1. Se coloca una baldosa 1x1 debajo de la textura para comprobar que efectivamente la textura tiene el tamaño 1x1
"""
Dibujar una textura en el plano suelo (plano xz)
La textura tiene un tamaño de 1x1
"""
import numpy as np
from fury import actor, window, io
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
YELLOW = np.array([1, 1, 0])
# -- Nombre de fichero con la textura
# -- Tamaño: 256x256 pixeles
FICHERO = 'ground.png'
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
# --- Crear el sistema coordenado
ejes = actor.axes()
# -- Cargar imagen de textura
try:
image = io.load_image(FICHERO)
except FileNotFoundError:
print(f"Error: No se encontró el archivo '{FICHERO}'")
exit()
# -------- Crear la textura
# -- El tamaño de la textura es de 256x256, igual que el de la imagen
# -- en píxeles
texture = actor.texture(image)
# -- Como queremos que tenga tamaño de 1x1 lo escalamos
# -- dividiendo por 256
texture.SetScale(1/256, 1/256, 1)
# -- Por defecto la textura se coloca en el plano xy
# -- La queremos en el xy, por lo que rotamos alrededor de x 90 grados
texture.SetOrientation(90, 0, 0)
# --- Crear baldosa 1x1
baldosa_size = np.array([1, 0.1, 1])
centers = np.array([ORIGEN])
colors = np.array([YELLOW])
baldosa = actor.cube(centers, colors=colors, scales=baldosa_size)
# -- Poner la baldosa debajo de la textura para comprobar su tamaño
baldosa.SetPosition(0, -0.2, 0)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(texture)
scene.add(baldosa)
# --- Cambiar la posición de la cámara
scene.GetActiveCamera().SetPosition(-50, 50, 80)
# --- Cambiar el color del fondo
scene.background([1, 1, 1])
# ---- Crear la ventana
window.show(scene, size=PANTALLA)- Resultado:

En este ejemplo se pone un suelo con una textura de 10x10. Como no he conseguido hacerlo igual que el array de baldosas, he creado una imagen haciendo copy&paste: ground_grid_10x10.jpg
"""
Dibujar una textura en el plano suelo (plano xz)
La textura tiene un tamaño de 10x10
"""
import numpy as np
from fury import actor, window, io
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
YELLOW = np.array([1, 1, 0])
LIGHTBLUE = np.array([0.9, 0.9, 1])
POS_BALDOSA = np.array([1, 0.05, 1])
# -- Nombre de fichero con la textura
# -- Tamaño: 2560x2560 pixeles
FICHERO = 'ground_grid_10x10.jpg'
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
# --- Crear el sistema coordenado
ejes = actor.axes()
# -- Cargar imagen de textura
try:
image = io.load_image(FICHERO)
except FileNotFoundError:
print(f"Error: No se encontró el archivo '{FICHERO}'")
exit()
# -------- Crear la textura
# -- El tamaño de cada cuadrícula de la textura es 256x256
texture = actor.texture(image)
# -- Como queremos que tenga tamaño de 1x1 lo escalamos
# -- dividiendo por 256
texture.SetScale(1/256, 1/256, 1)
# -- Por defecto la textura se coloca en el plano xy
# -- La queremos en el xy, por lo que rotamos alrededor de x 90 grados
texture.SetOrientation(90, 0, 0)
# --- Crear baldosa 1x1
baldosa_size = np.array([1, 0.1, 1])
centers = np.array([POS_BALDOSA])
colors = np.array([YELLOW])
baldosa = actor.cube(centers, colors=colors, scales=baldosa_size)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(texture)
scene.add(baldosa)
# --- Cambiar la posición de la cámara
scene.GetActiveCamera().SetPosition(-50, 50, 80)
# --- Cambiar el color del fondo
scene.SetBackground(LIGHTBLUE)
# ---- Crear la ventana
window.show(scene, size=PANTALLA)- Resultado:


En este ejemplo hacemos rotar la baldosa alrededor del eje Y. Para ello hay que poner una función de retrollamada (callback) que se ejecuta cada 200ms, en esa función se rota la baldosa y se renderiza la nueva imagen
"""
Animación de la baldosa: se hace rotar alrededor del eje Y
"""
import numpy as np
from fury import actor, window, io
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
YELLOW = np.array([1, 1, 0])
LIGHTBLUE = np.array([0.9, 0.9, 1])
# -- Fichero con la textura de la baldosa
FICHERO = 'ground_grid_10x10.jpg'
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
# --- Crear el sistema coordenado
ejes = actor.axes()
# -- Cargar imagen de textura
try:
image = io.load_image(FICHERO)
except FileNotFoundError:
print(f"Error: No se encontró el archivo '{FICHERO}'")
exit()
# -------- Crear la textura
# -- El tamaño de cada cuadrícula de la textura es 256x256
texture = actor.texture(image)
# -- Como queremos que tenga tamaño de 1x1 lo escalamos
# -- dividiendo por 256
texture.SetScale(1/256, 1/256, 1)
# -- Por defecto la textura se coloca en el plano xy
# -- La queremos en el xy, por lo que rotamos alrededor de x 90 grados
texture.SetOrientation(90, 0, 0)
# --- Crear baldosa 1x1
baldosa_size = np.array([1, 0.1, 1])
centers = np.array([ORIGEN])
colors = np.array([YELLOW])
baldosa = actor.cube(centers, colors=colors, scales=baldosa_size)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(texture)
scene.add(baldosa)
# --- Cambiar la posición de la cámara
scene.GetActiveCamera().SetPosition(-5, 5, 8)
# --- Cambiar el color del fondo
scene.SetBackground(LIGHTBLUE)
# ------- Crear la ventana (sin visualizar todavía)
showm = window.ShowManager(
scene=scene, size=PANTALLA,
)
# -- Funcion de retrollamada para realizar la animación
# -- Se ejecuta cada 200 ms
def timer_callback(_obj, _event):
# -- Girar la baldosa
baldosa.RotateY(5)
# -- Actualizar la escena
showm.render()
# -- Configurar la función de retrollamada
showm.add_timer_callback(True, 200, timer_callback)
# -- Visualizar la escena. Que comience la fiesta!
showm.start()- Resultado: Previamente a generar la animación se ha hecho un zoom hacia la baldosa

Este es el mismo ejemplo de la baldosa (sin animación), pero ahora se aumenta el zoom automáticamente
"""
Situar una baldosa en el plano xz
Hacer zoom y mostrar la escena
"""
import numpy as np
from fury import actor, window, io
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
YELLOW = np.array([1, 1, 0])
LIGHTBLUE = np.array([0.9, 0.9, 1])
# -- Fichero con la textura de la baldosa
FICHERO = 'ground_grid_10x10.jpg'
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
# --- Crear el sistema coordenado
ejes = actor.axes()
# -- Cargar imagen de textura
try:
image = io.load_image(FICHERO)
except FileNotFoundError:
print(f"Error: No se encontró el archivo '{FICHERO}'")
exit()
# -------- Crear la textura
# -- El tamaño de cada cuadrícula de la textura es 256x256
texture = actor.texture(image)
# -- Como queremos que tenga tamaño de 1x1 lo escalamos
# -- dividiendo por 256
texture.SetScale(1/256, 1/256, 1)
# -- Por defecto la textura se coloca en el plano xy
# -- La queremos en el xy, por lo que rotamos alrededor de x 90 grados
texture.SetOrientation(90, 0, 0)
# --- Crear baldosa 1x1
baldosa_size = np.array([1, 0.1, 1])
centers = np.array([ORIGEN])
colors = np.array([YELLOW])
baldosa = actor.cube(centers, colors=colors, scales=baldosa_size)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(texture)
scene.add(baldosa)
# --- Cambiar la posición de la cámara
scene.GetActiveCamera().SetPosition(-5, 5, 8)
# --- Cambiar el color del fondo
scene.SetBackground(LIGHTBLUE)
# ------- Crear la ventana (sin visualizar todavía)
showm = window.ShowManager(scene=scene, size=PANTALLA)
# -- Modificar el zoom
scene.zoom(5)
# -- Visualizar la escena. Que comience la fiesta!
showm.start()- Resultado:

Ahora que ya sé manejar mínimamente Fury, voy a profundizar más sobre el bajo nivel. La idea es construir objetos básicos desde cero, usando puntos. Quiero ir aprendiendo todos los conceptos relacionados...
Modificamos el ejemplo anterior de la baldosa para utilizar un sistema de coordenadas con el eje Z apuntando hacia arriba, que es la orientación que suelen utilizar los programa CAD, como FreeCAD
La clave está en el método camera.SetViewUp() que indica cuál es la dirección "arriba"
"""
Situar una baldosa en el plano xy
Hacer zoom y mostrar la escena
"""
import numpy as np
from fury import actor, window, io
# ------- Constantes
ORIGEN = np.array([0, 0, 0])
YELLOW = np.array([1, 1, 0])
LIGHTBLUE = np.array([0.9, 0.9, 1])
# -- Fichero con la textura de la baldosa
FICHERO = 'ground_grid_10x10.jpg'
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# ------- Construir los objetos a visualizar
# --- Crear el sistema coordenado
ejes = actor.axes()
# -- Cargar imagen de textura
try:
image = io.load_image(FICHERO)
except FileNotFoundError:
print(f"Error: No se encontró el archivo '{FICHERO}'")
exit()
# -------- Crear la textura
# -- El tamaño de cada cuadrícula de la textura es 256x256
texture = actor.texture(image)
# -- Como queremos que tenga tamaño de 1x1 lo escalamos
# -- dividiendo por 256
texture.SetScale(1/256, 1/256, 1)
# --- Crear baldosa 1x1
baldosa_size = np.array([1, 1, 0.1])
centers = np.array([ORIGEN])
colors = np.array([YELLOW])
baldosa = actor.cube(centers, colors=colors, scales=baldosa_size)
# --- Crear la escena con los elementos
scene = window.Scene()
scene.add(ejes)
scene.add(texture)
scene.add(baldosa)
# --- Cambiar la posición de la cámara
cam = scene.GetActiveCamera()
cam.SetPosition(5, -1, 3)
cam.SetViewUp(0, 0, 1)
# --- Cambiar el color del fondo
scene.SetBackground(LIGHTBLUE)
# ------- Crear la ventana (sin visualizar todavía)
showm = window.ShowManager(scene=scene, size=PANTALLA)
# -- Modificar el zoom
scene.zoom(3)
# -- Visualizar la escena. Que comience la fiesta!
showm.start()- Resultado:

Vamos al bajo nivel. La superficie mínima a dibujar es el triángulo. Para definirlo tenemos que especificar las coordenadas de sus 3 vértices. Vamos a comenzar por el triángulo más sencillo: un triángulo rectángulo con un vértice en el origen de coordenadas (0,0,0) y los otros dos en los ejes x e y. Es por tanto un triángulo que está sobre el suelo (plano xy)
Para definir ese triángulo usamos una lista con los vértices:
p0 = (0,0,0)
p1 = (1,0,0)
p2 = (0,1,0)
vertices = [p0, p1, p2]Además necesitamos definir la orientación del triángulo. Esto es, necesitamos saber cuál es la cara exterior y cuál la interior. Ahora de momento es un único triángulo, pero con varios de ellos se genera una superficie y hay que distinguir las caras que dan al exterior de las del interior
La orientación la definimos mediante su vector normal.Es un vector perpendicular al triángulo que apunta hacia el exterior. En el caso de este triángulo de ejemplo, su normal es el el eje z
La orientación la definimos mediante el orden de los vertices. Si el triángulo se dibuja en el orden P0, P1 y P2, la normal es el eje z. El triángulo queda entonces totalmente definido con el orden de los vértices, que definimos en la lista triángulo
El triángulo se define, por tanto, con estas dos listas: una con todos los vértices, y otra con el órden de los vertices
vertices = [
[0,0,0],
[1,0,0],
[0,1,0]
]
triangulo = [0, 1, 2]La normal se usa para saber si la cara es visible o no. Se supone que es visible en la dirección hacia donde apunta la normal
Además, la normal se utiliza para determina la cantidad de luz que incide sobre la cara visible. Si la dirección de la luz incidente es paralela a la normal, significa que incide la máxima cantidad de luz, y por tanto brillará más. Por contra, si la luz incide con un ángulo menor, estará más oscura. En el caso de una incidencia de 90 grados, la cara NO recibe luz, y por tanto estará totalmente oscura
En este ejemplo dibujamos un triángulo a bajo nivel, a partir de sus vértices
"""
Dibujo de un triángulo en el plano xy, a partir de sus vértices.
"""
import numpy as np
from fury import window, lib, utils, actor
# ------- Constantes
ORIGIN = np.array([0.0, 0.0, 0.0])
LIGHTBLUE = np.array([0.5, 0.5, 1])
YELLOW = np.array([1, 1, 0])
PANTALLA = (600, 400)
# -- Definir los vértices del triángulo
vertices = np.array(
[
[0, 0, 0], # Vértice 0
[1, 0, 0], # Vértice 1
[0, 1, 0], # Vertice 2
]
)
# -- El triángulo está formado por 3 vértices. El orden determina la normal,
# -- y por tanto qué lado es visible y cuál no
triangles = np.array(
[
[0, 1, 2],
],
)
# -- En Fury usamos esta clase para crear un objeto a partir de
# -- de sus vértices
polydata = lib.PolyData()
# -- Añadir los vértices y triángulos al objeto
utils.set_polydata_vertices(polydata, vertices)
utils.set_polydata_triangles(polydata, triangles)
# -- Obtener el triángulo listo para usarse
triangulo = utils.get_actor_from_polydata(polydata)
# -- Además del triángulo queremos visualizar los vértices
puntos = actor.point(vertices, point_radius=0.05, colors=YELLOW)
# --- Crear el sistema coordenado
ejes = actor.axes()
# Crear la escena
scene = window.Scene()
scene.add(triangulo)
scene.add(puntos)
scene.add(ejes)
# --- Cambiar la posición de la cámara
cam = scene.GetActiveCamera()
cam.SetPosition(5, -1, 5)
cam.SetViewUp(0, 0, 1) # -- Eje Z hacia arriba
scene.SetBackground(LIGHTBLUE)
scene.zoom(2)
# ------- Crear la ventana (sin visualizar todavía)
showm = window.ShowManager(scene=scene, size=PANTALLA, reset_camera=False)
# -- Visualizar la escena. Que comience la fiesta!
showm.start()Este es el resultado:

Si rotamos la cámara y miramos la parte inferior del triángulo, veremos que desaparece. Esto es porque estamos mirando su parte trasera, que es interior. Sólo es visible la exterior. Los vértices sí que se ven

En este ejemplo el triángulo que se dibujo es visible tanto por la parte superior como la inferior, ya te que tiene 2 normales La clave es definir el triángulo con dos caras, una superior y otra inferior. Los vértices son los mismos, pero el triángulo superior los vértices están en sentido antihorario (para que la normal vaya hacia el eje z). La cara inferior está en sentido horario, y por tanto la normal apunta hacia el eje -z
"""
Dibujo de un triángulo en el plano xy, a partir de sus vértices
El triángulo tiene doble cara: las dos son exteriores y visibles
"""
import numpy as np
from fury import window, lib, utils, actor
# ------- Constantes
ORIGIN = np.array([0.0, 0.0, 0.0])
LIGHTBLUE = np.array([0.5, 0.5, 1])
YELLOW = np.array([1, 1, 0])
PANTALLA = (600, 400)
# -- Definir los vértices del triángulo
vertices = np.array(
[
[0, 0, 0], # Vértice 0
[1, 0, 0], # Vértice 1
[0, 1, 0], # Vertice 2
]
)
# -- El triángulo está formado por 3 vértices. El orden determina la normal,
# -- y por tanto qué lado es visible y cuál no
triangles = np.array(
[
[0, 1, 2], # -- Cara exterior superior (normal eje z)
[2, 1, 0] # -- Cara exterior inferior (normal eje -z)
],
)
# -- En Fury usamos esta clase para crear un objeto a partir de
# -- de sus vértices
polydata = lib.PolyData()
# -- Añadir los vértices y triángulos al objeto
utils.set_polydata_vertices(polydata, vertices)
utils.set_polydata_triangles(polydata, triangles)
# -- Obtener el triángulo listo para usarse
triangulo = utils.get_actor_from_polydata(polydata)
# -- Además del triángulo queremos visualizar los vértices
puntos = actor.point(vertices, point_radius=0.05, colors=YELLOW)
# --- Crear el sistema coordenado
ejes = actor.axes()
# Crear la escena
scene = window.Scene()
scene.add(triangulo)
scene.add(puntos)
scene.add(ejes)
# --- Cambiar la posición de la cámara
cam = scene.GetActiveCamera()
cam.SetPosition(5, -1, 5)
cam.SetViewUp(0, 0, 1) # -- Eje Z hacia arriba
scene.SetBackground(LIGHTBLUE)
scene.zoom(2)
# ------- Crear la ventana (sin visualizar todavía)
showm = window.ShowManager(scene=scene, size=PANTALLA, reset_camera=False)
# -- Visualizar la escena. Que comience la fiesta!
showm.start()- Este es el resultado:

Ahora vemos el triángulo de la parte inferior, y es también visible

Vamos a dibujar esta superficie mínima, el triángulo en FreeCAD. Utilizaremos los mismos vértices y la misma orientación que en el ejemplo anterior

Lo dibujamos en Freecad en el banco de trabajo Draft, utilizando una polilinea que genera un objeto de tipo Wire. Los puntos de esta polilínea son los vértices del triángulo, y se ha dibujado en sentido antihorario para que la normal apunte hacia arriba y la cara superior sea la visible
Activamos el Grid y situamos un vértice en el origen y los otros dos en el grid, a la distancia de 1mm (1 unidad)
Exportamos el triángulo en formato STL, tanto binario como en ascii
- Experimen-01-triangle.slt: Binario
- Experimen-01-triangle.asl: Ascii
El triángulo en STL ascii (ast) es un fichero de texto que se puede ver. Este es su contenido:
solid Mesh
facet normal 0.000000 0.000000 1.000000
outer loop
vertex 0.000000 1.000000 0.000000
vertex 0.000000 0.000000 0.000000
vertex 1.000000 0.000000 0.000000
endloop
endfacet
endsolid Mesh
Comprobamos que efectivamente la normal es el eje z, y los vértices que tiene definidos son los que teníamos del anterior triángulo: un vértice en el origen, otro en el eje x, y otro en el y
El markdown de la wiki tiene un visualizador de STL:
solid Mesh
facet normal 0.000000 0.000000 1.000000
outer loop
vertex 0.000000 1.000000 0.000000
vertex 0.000000 0.000000 0.000000
vertex 1.000000 0.000000 0.000000
endloop
endfacet
endsolid Mesh
Comprobamos que al rotar el triángulo, sólo se ve su cara superior, pero no por abajo
En este ejemplo leemos el fichero STL creado en el experimento 1 y lo renderizamos en Fury. Además obtenemos sus vértices y los pintamos de amarillo. El triángulo le asignamos el color naranja
Fury puede leer los STLs tanto en binario como en ascii, PERO tienen que tener la extensión STL. Si usamos AST fury NO LO RECONOCERÁ. Hay que renombrarlo a STL
"""
Dibujo un triángulo apoyado en el plano XY,
a partir de un fichero STL
"""
import numpy as np
from fury import window, io, utils, actor
# ------- Constantes
ORIGIN = np.array([0.0, 0.0, 0.0])
LIGHTBLUE = np.array([0.5, 0.5, 1])
YELLOW = np.array([1, 1, 0])
ORANGE = np.array([1, 0.5, 0])
PANTALLA = (600, 400)
# -- Crear el triángulo a partir del fichero stl
polydata = io.load_polydata("Experim-01-triangle.stl")
triangulo = utils.get_actor_from_polydata(polydata)
# -- Establecer el color del triángulo
triangulo.GetProperty().SetColor(ORANGE)
# -- Obtener los vértices del triángulo
# -- Es para dibujarlos también
vertices = utils.get_polydata_vertices(polydata)
# -- Obtener los vértices como un actor para visualizarlo
puntos = actor.point(vertices, point_radius=0.05, colors=YELLOW)
# --- Crear el sistema coordenado
ejes = actor.axes()
# Crear la escena
scene = window.Scene()
scene.add(triangulo)
scene.add(puntos)
scene.add(ejes)
# --- Cambiar la posición de la cámara
cam = scene.GetActiveCamera()
cam.SetPosition(5, -1, 5)
cam.SetViewUp(0, 0, 1) # -- Eje Z hacia arriba
scene.SetBackground(LIGHTBLUE)
scene.zoom(2)
# ------- Crear la ventana (sin visualizar todavía)
showm = window.ShowManager(scene=scene, size=PANTALLA, reset_camera=False)
# -- Visualizar la escena. Que comience la fiesta!
showm.start()Este es el resultado:

Si miramos desde abajo el triángulo NO SE VE. Porque la normal apunta hacia el eje z

En este ejemplo se dibuja una línea naranja de 2 puntos. Los puntos también se dibujan, así como el eje de coordenadas
"""
Dibujar una línea en el plano xy
"""
import numpy as np
from fury import window, actor
# ------- Constantes
ORIGIN = np.array([0.0, 0.0, 0.0])
LIGHTBLUE = np.array([0.5, 0.5, 1])
YELLOW = np.array([1, 1, 0])
ORANGE = np.array([1, 0.5, 0])
PANTALLA = (600, 400)
# -- Definir la línea
lines = [np.array([[0, 0, 0], [1, 1, 0]])]
linea = actor.line(lines, colors=ORANGE, linewidth=5)
# -- Dibujar los puntos de la línea
puntos = actor.point(lines[0], point_radius=0.05, colors=YELLOW)
# Crear la escena
scene = window.Scene()
scene.add(actor.axes()) # -- Ejes
scene.add(linea)
scene.add(puntos)
# --- Cambiar la posición de la cámara
cam = scene.GetActiveCamera()
cam.SetPosition(5, -1, 5)
cam.SetViewUp(0, 0, 1) # -- Eje Z hacia arriba
scene.SetBackground(LIGHTBLUE)
scene.zoom(2)
# ------- Crear la ventana (sin visualizar todavía)
showm = window.ShowManager(scene=scene, size=PANTALLA, reset_camera=False)
# -- Visualizar la escena. Que comience la fiesta!
showm.start()- Este es el resultado:

Dibujar una línea 3D, uniendo sus 4 puntos
"""
Dibujar una poly-línea de 4 puntos
"""
import numpy as np
from fury import window, actor
# ------- Constantes
ORIGIN = np.array([0.0, 0.0, 0.0])
LIGHTBLUE = np.array([0.5, 0.5, 1])
YELLOW = np.array([1, 1, 0])
ORANGE = np.array([1, 0.5, 0])
PANTALLA = (600, 400)
# -- Definir todos los puntos de la poli-linea
lines = [np.array([[0, 0, 0], [1, 1, 0], [1, 1, 1], [1, 2, 1]])]
linea = actor.line(lines, colors=[ORANGE], linewidth=5)
# -- Dibujar los puntos de la poli-línea
puntos = actor.point(lines[0], point_radius=0.05, colors=YELLOW)
# Crear la escena
scene = window.Scene()
scene.add(actor.axes()) # -- Ejes
scene.add(linea)
scene.add(puntos)
# --- Cambiar la posición de la cámara
cam = scene.GetActiveCamera()
cam.SetPosition(5, -1, 5)
cam.SetViewUp(0, 0, 1) # -- Eje Z hacia arriba
scene.SetBackground(LIGHTBLUE)
scene.zoom(1)
# ------- Crear la ventana (sin visualizar todavía)
showm = window.ShowManager(scene=scene, size=PANTALLA, reset_camera=False)
# -- Visualizar la escena. Que comience la fiesta!
showm.start()- Resultado:

En este ejemplo se dibujan dos líneas independientes en el plano xy
"""
Dibujar dos líneas
"""
import numpy as np
from fury import window, actor
# ------- Constantes
ORIGIN = np.array([0.0, 0.0, 0.0])
LIGHTBLUE = np.array([0.5, 0.5, 1])
YELLOW = np.array([1, 1, 0])
ORANGE = np.array([1, 0.5, 0])
MAGENTA = np.array([1, 0, 1])
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# -- Definir los puntos de las dos líneas
lines = [
np.array([[0, 0, 0], [1, 1, 0]]), # -- Linea 1
np.array([[0, 1, 0], [1, 2, 0]]) # -- Linea 2
]
# -- Actor con las líneas
lineas = actor.line(lines, colors=[ORANGE, ORANGE], linewidth=5)
# -- Obtener los puntos de las lineas
puntos0 = actor.point(lines[0], point_radius=0.05, colors=YELLOW)
puntos1 = actor.point(lines[1], point_radius=0.05, colors=MAGENTA)
# Crear la escena
scene = window.Scene()
scene.add(actor.axes()) # -- Ejes
scene.add(lineas)
scene.add(puntos0, puntos1)
# --- Cambiar la posición de la cámara
cam = scene.GetActiveCamera()
cam.SetPosition(5, -1, 5)
cam.SetViewUp(0, 0, 1) # -- Eje Z hacia arriba
scene.SetBackground(LIGHTBLUE)
scene.zoom(1)
# ------- Crear la ventana (sin visualizar todavía)
showm = window.ShowManager(scene=scene, size=PANTALLA, reset_camera=False)
# -- Visualizar la escena. Que comience la fiesta!
showm.start()- Resultado:

En este ejemplo se dibuja un mundo virtual formado por un grid de 10x10 lìneas
"""
Dibujar un grid de 10x10 líneas
"""
import numpy as np
from fury import window, actor
# ------- Constantes
ORIGIN = np.array([0.0, 0.0, 0.0])
GREEN = np.array([0, 1, 0])
# -- Semi-longitud de las líneas
L = 10
# -- Tamaño de la ventana
PANTALLA = (600, 400)
# -- Definir los puntos de las dos líneas
lines = [
# -- Lineas horizontales
np.array([[-L, -10, 0], [L, -10, 0]]),
np.array([[-L, -9, 0], [L, -9, 0]]),
np.array([[-L, -8, 0], [L, -8, 0]]),
np.array([[-L, -7, 0], [L, -7, 0]]),
np.array([[-L, -6, 0], [L, -6, 0]]),
np.array([[-L, -5, 0], [L, -5, 0]]),
np.array([[-L, -4, 0], [L, -4, 0]]),
np.array([[-L, -3, 0], [L, -3, 0]]),
np.array([[-L, -2, 0], [L, -2, 0]]),
np.array([[-L, -1, 0], [L, -1, 0]]),
np.array([[-L, 0, 0], [L, 0, 0]]),
np.array([[-L, 1, 0], [L, 1, 0]]),
np.array([[-L, 2, 0], [L, 2, 0]]),
np.array([[-L, 3, 0], [L, 3, 0]]),
np.array([[-L, 4, 0], [L, 4, 0]]),
np.array([[-L, 5, 0], [L, 5, 0]]),
np.array([[-L, 6, 0], [L, 6, 0]]),
np.array([[-L, 7, 0], [L, 7, 0]]),
np.array([[-L, 8, 0], [L, 8, 0]]),
np.array([[-L, 9, 0], [L, 9, 0]]),
np.array([[-L, 10, 0], [L, 10, 0]]),
# -- Lineas verticales
np.array([[-10, -L, 0], [-10, L, 0]]),
np.array([[-9, -L, 0], [-9, L, 0]]),
np.array([[-8, -L, 0], [-8, L, 0]]),
np.array([[-7, -L, 0], [-7, L, 0]]),
np.array([[-6, -L, 0], [-6, L, 0]]),
np.array([[-5, -L, 0], [-5, L, 0]]),
np.array([[-4, -L, 0], [-4, L, 0]]),
np.array([[-3, -L, 0], [-3, L, 0]]),
np.array([[-2, -L, 0], [-2, L, 0]]),
np.array([[-1, -L, 0], [-1, L, 0]]),
np.array([[0, -L, 0], [0, L, 0]]),
np.array([[1, -L, 0], [1, L, 0]]),
np.array([[2, -L, 0], [2, L, 0]]),
np.array([[3, -L, 0], [3, L, 0]]),
np.array([[4, -L, 0], [4, L, 0]]),
np.array([[5, -L, 0], [5, L, 0]]),
np.array([[6, -L, 0], [6, L, 0]]),
np.array([[7, -L, 0], [7, L, 0]]),
np.array([[8, -L, 0], [8, L, 0]]),
np.array([[9, -L, 0], [9, L, 0]]),
np.array([[10, -L, 0], [10, L, 0]]),
]
# -- Actor con las líneas
lineas = actor.line(lines, colors=[GREEN]*len(lines), linewidth=3)
# Crear la escena
scene = window.Scene()
scene.add(actor.axes()) # -- Ejes
scene.add(lineas)
# --- Cambiar la posición de la cámara
cam = scene.GetActiveCamera()
cam.SetPosition(5, -1, 5)
cam.SetViewUp(0, 0, 1) # -- Eje Z hacia arriba
scene.zoom(1)
# ------- Crear la ventana (sin visualizar todavía)
showm = window.ShowManager(scene=scene, size=PANTALLA, reset_camera=False)
# -- Visualizar la escena. Que comience la fiesta!
showm.start()- Resultado:

🚧 DEBUG 🚧
- Mismo ejemplo que el 26, pero con bucles (refactorizar con ia)
import numpy as np
L = 10 # Asumo que L está definido en algún lugar de tu código
y_coords = np.arange(-10, 11)
lines = [np.array([[-L, y, 0], [L, y, 0]]) for y in y_coords]
print(lines)- Otro ejemplo: Un tetraedro
- Otro ejemplo: Un cubo