Gracias Gracias:  1
Página 2 de 2 PrimerPrimer 12
Resultados 16 al 19 de 19

Tema: Tecnologias-libres-para-sintesis-de-imagen-digital-tridimensional-blendiberia-2006

  1. #16
    Administrador y fundador. Avatar de 3dpoder
    Fecha de ingreso
    Apr 2002
    Mensajes
    15,460

    Blender Exportación desde Blender

    Exportación desde Blender
    Fernando Arroba Rubio
    gnotxor@gmail.com ::


    Este trabajo pretende exponer de una manera sencilla y amena los métodos de exportación de objetos desde Blender a cualquier formato externo. Como ejemploilustrativo se ha seleccionado el lenguaje VRML v2.0 por dosmotivos fundamentales: Es un lenguaje muy extendido e independiente del sistema operativo de la máquina, pero principalmente es un lenguaje de descripción de espacios virtuales comprensible para los humanos. Los ficheros se guardan en formato de texto y se pueden modificar y crear concualquier editor de texto.

    El trabajo hace hincapié en la estructura que debe tener un script exportación desde Blender, aunque algunas de las apreciaciones son subjetivas. Para no desviar la atención sobre el eje central del mismo no se tratan temas como la “herencia” cuando habla de las clases en la exportación. De la misma manera tampoco se hacen referencias exhaustivas al lenguaje VRML para el cual es recomendable consultar algún tipo de documentación propia. Tampoco se ha pretendido hacer un repaso exhaustivo de las matemáticas que envuelven al 3D y en lo posible se obviaran de la misma manera que se ha intentado evitar complejidades de este en el script.

    Entrando en el menú sin hacer mucho ruido
    El primer punto importante para que Blender reconozca un script es la primera línea del código. Paralelamente a lo que hacen los script de python normales, que lanzan el intérprete, Blender lanza el suyo propio:

    1 #!BPY
    A continuación de esta línea podemos añadir un comentario del script que añada funcionalidad a dicha identificación. Es decir, podemos hacer un registro del mismo en los menús de Blender.

    2 """Registramos el script en los menús de Blender
    3 Name: 'Ejemplo de la conferencia...'
    4 Blender: 234
    5 Group: 'Export'
    6 Tooltip: 'Este es el ejemplo para la conferencia'
    7 """
    En este comentario especificamos el texto que aparecerá en el menú (3), la versión para la que esta escrito (4), el grupo al que pertenece el script (5) y la etiqueta de ayuda que aparece cuando situamos el cursor sobre la entrada del menú (6).
    Si la versión de Blender sobre la que corremos el script es anterior a la que marca la cabecera nos mostrara un aviso de que podría no funcionar correctamente:

    Version mismatch: script was written for Blender 234. It may fail with yours: 232Version mismatch: script was written for Blender 234. It may fail with yours: 232

    Es posible añadir más información a dicha cabecera, como el poder dar opciones al usuario a la hora de exportar. Por ejemplo, si queremos que el usuario pueda seleccionar si quiere exportar toda la escena o sólo aquellos objetos que se encuentren seleccionados, podríamos añadir submenús de la siguiente manera:

    8 """Registramos el script en los menús de Blender
    9 Name: 'Ejemplo de la conferencia...'
    10 Blender: 234
    11 Group: 'Export'
    12 Submenu: 'Todos los objetos...' todos
    13 Submenu: 'Objetos seleccionados...' selec
    14 Tooltip: 'Este es el ejemplo para la conferencia'
    15 """
    Si utilizamos submenús (12-13) debemos capturar el valor de los mismos (todos o selec). Como no todas las versiones de Blender pueden capturar ese argumento es mejor tener algún tipo de código que controle las excepciones que puede provocar. Vamos a ver el ejemplo, esta vez con el código completo:

    16 """Registramos el script en los menús de Blender
    17 Name: 'Ejemplo de la conferencia...'
    18 Blender: 234
    19 Group: 'Export'
    20 Submenu: 'Todos los objetos...' todos
    21 Submenu: 'Objetos seleccionados...' selec
    22 Tooltip: 'Este es el ejemplo para la conferencia'
    23 """
    24 # ---------
    25 # MAIN
    26 # ---------
    27 try:
    28 ARG = __script__['arg'] # Capturo el argumento del
    submenú
    29 except:
    30 print "Versión antigua"
    31 ARG = 'selec'
    32
    33 print "Se ha seleccionado la opción: %s" % ARG}
    Lo más interesante a destacar en el código es el manejo de excepciones (27-31) que se hace para cargar en una variable global ARG (28 la selección del usuario. Al manejar la excepción podemos mantener en funcionamiento el script y así poder asignarle un valor por defecto a la variable (31). En la línea 33 podemos observar que mostramos a través de la consola ese argumento como una cadena de caracteres más. Es mucho más interesante, por supuesto, si utilizamos dicho valor para hacer cosas distintas, por ejemplo:

    34 if ARG == 'todos':
    35 losObjetos = Scene.getCurrent().getChildren()
    36 else:
    37 losObjetos = Blender.Object.GetSelected()
    Es decir, si el argumento pasado es “todos” se cargan todos los objetos de la escena actual en una lista llamada “losObjetos”, mientras que si el argumento es otro, se cargaran solo los objetos que estén seleccionados. Recordamos que si se produce la excepción que no permite cargar el argumento desde el submenú, el script cargara por defecto sólo los objetos seleccionados.

    Por otro lado, es muy recomendable que tengamos algún código que controle la versión de Blender que se esta ejecutando. Aunque ya hemos comentado que podemos (debemos) establecer la versión de Blender para la que se ha hecho el script y que el propio programa tiene un cierto control sobre ello, no esta de más que añadamos más seguridad a nuestro script de forma manual. Esto evitara problemas y sinsabores al usuario de una forma sencilla y elegante. Por ejemplo, si sabemos que nuestro script no funciona en alguna versión determinada podríamos avisar al usuario para evitar que pruebe una y otra vez sin conseguir nada. Es frustrante cuando lanzas un script y no hace lo que esperas y además tampoco sabes el porqué. Por ejemplo, si sabemos que estamos utilizando características que aparecieron en el API a partir de la versión 2.30 podríamos escribir un poco de código de la siguiente manera, y comenzar realmente el procesamiento del fichero a partir de la línea 42:

    38 if Blender.Get('versión') < 230:
    39 print "Aviso: ¡Versión incorrecta de blender!"
    40 print "No esta utilizando Blender 2.30 o superior,
    descargue"
    41 print "una versión más actualizada desde
    http://blender.org/"
    42 else:
    43 .
    Un vistazo a lo que necesitamos
    Antes de comenzar a detallar algunos módulos que debemos emplear de forma casi sistematica, necesitamos comprender un poco cómo esta organizada la información que queremos exportar. También debemos tener claro el formato en el que debe ser escrita. Esto en algunas ocasiones (por no decir la mayoría) no coincide. Para hacer una exportación limpia muchas veces deberemos tener en cuenta esas peculiaridades y adaptarlas lo mejor posible.

    Un poco de matemáticas
    Un primer foco de problemas puede venir de la geometría de los objetos. En algunas ocasiones lo podremos reducir a una cuestión de equivalencias de escala, en otras necesitaremos un conocimiento un poco más preciso de las matemáticas que envuelven el 3D.

    Para empezar vamos a echar un vistazo a la matriz de los objetos. Comenzamos para ello con la representación de un punto. Un punto no tiene dimensión, simplemente es una posición (coordenadas) en un espacio 3D. Sin embargo, la misma posición puede estar representada por valores distintos dependiendo del origen de coordenadas que utilicemos. Muchos sistemas y programas que representan figuras en un espacio 3D siguen pautas parecidas: almacenan la descripción de una malla con respecto a un punto (que llamaremos centro) y añaden una matriz de modificación. En esa matriz se guardan informaciónes tan valiosas como la orientación, posición y escalado de la malla. Por último, para conocer la posición real de un punto de esa malla sólo hay que aplicar dicha matriz a las coordenadas relativas de dicho punto dentro de la malla.

    Puede parecer algo alejado del usuario normal, pero no lo es, basta pulsar “n” en modo objeto para que blender nos muestre los valores de la matriz del objeto seleccionado (aunque no la matriz misma).

    Otro tema es la orientación de los ejes de coordenadas. Concretamente si son ejes de coordenadas “a derechas”o“a izquierdas”. Blender utiliza un sistema orientado a derechas, es decir, “x” crece hacia la derecha, “y” crece hacia arriba y “z” crece hacia afuera de la pantalla. Otros programas, como POV-Ray utilizan un sistema orientado a izquierdas, es decir, “x” e “y” están orientados a derecha y arriba, respectivamente y “z” crece hacia adentro de la pantalla.

    Hay que tener en cuenta todos estos detalles a la hora de exportar a otros sistemas, pues sus valores pueden representarse de otra manera. Antes debíamos realizar todos los cálculos necesarios por nosotros mismos (producto escalar, producto de vectores, calculo del módulo de un vector, etc). En la actualidad contamos con un módulo de utilidades matemáticas dentro de blender: Mathutils.

    Blender (la madre del cordero)
    Este módulo es el base de todos los demás. Por otro lado, también proporciona información útil a la hora de la exportación. Por ejemplo, si queremos proporcionar al usuario un nombre de fichero donde guardar la exportación, podemos solicitar a Blender un nombre de fichero.

    44 nombreFichero = Blender.Get('filename')
    Guarda en la variable “nombreFichero” el nombre del último archivo que se ha abierto o guardado desde blender. Otra de las informaciónes útiles que proporciona este módulo es la versión de blender que esta en funcionamiento.

    45 blenderVer = Blender.Get('versión')
    A partir de este módulo se haran las importaciones.

    46 import Blender
    47 from Blender import NMesh, Window
    48 from Blender import Scene, Mathutils
    49 ....
    Es aconsejable utilizar importaciones desde el módulo principal y no hacer importaciones globales con el comodín “*” para no llenar el diccionario del espacio de nombres de python con demasiadas entradas, muchas de ellas inútiles.

    Por otro lado, el obligarnos a utilizar el nombre del módulo al que pertenece alguna función, o clase, hará más legible el código que escribamos, sobre todo a la hora de revisiones posteriores.

    Facilitando la vida al usuario
    Hay ocasiones en que el programador hace el script para uso propio. Esto conlleva la tendencia a interactuar con él desde el propio código, sin embargo, hace prácticamente imposible que un tercero utilice dicho script.

    En otras ocasiones estamos obligados a pensar en cómo otra persona, que no tienen porqué saber nada sobre programación, conseguira aprovechar el trabajo que realiza el script. Para facilitarnos el trabajo, Blender provee de dos módulos interesantes: Sys y Window. En nuestro ejemplo utilizamos tan sólo uno de ellos para preguntar el nombre del fichero donde guardará la salida y comenzara el proceso al pulsar el botón correspondiente.

    50 Window.FileSelector(fichero, "Exportar")
    Esta línea de código muestra en la ventana de Blender un diálogo de fichero. El primer argumento de FileSelector es la función que se ejecutara si se pulsa el botón del diálogo; a dicha función le pasara el nombre del fichero seleccionado. El segundo argumento es el texto que aparecerá en dicho botón. Lo que nos interesara, por supuesto, es la estructura de la función que realiza el trabajo.

    51 def fichero(nomFichero):
    52 if nomFichero.find('.wrl', -4) < 0:
    53 nomFichero += '.wrl'
    54 # Abro el fichero y lo conecto con std
    55 f = open(nomFichero, 'w')
    56 std = sys.stdout
    57 sys.stdout = f
    58 # Proceso los datos
    59 sce = ExportadorEscena(nomFichero)
    60 sce.proceso()
    61 # Devuelvo el control de stdout y cierro el fichero
    62 sys.stdout = std
    63 f.close()
    Lo primero que hace dicha función es comprobar el nombre del fichero que nos proporciona el selector de ficheros de Blender. Principalmente la extensión, hacemos esto para evitar pérdidas de datos producidas por una selección errónea por parte del usuario. Es posible que al seleccionar el nombre de un fichero éste marque equivocadamente el fichero .blend de origen, o cualquier otro. De esta manera, si el nombre del fichero no concluye con la extensión deseada, esta será añadida salvaguardando de la sobrescritura datos ajenos al script y el proceso que representa.

    En las siguientes líneas (54-63) se realiza todo el proceso. Basicamente en tres pasos:


    • Se abre un fichero y se redirecciona la salida estándar a él (55-57).


    • Se realiza el proceso de exportación (59, 60).


    • Se devuelve la salida estándar a su estado original (62,63).


    Esto facilita en gran medida la legibilidad del código pues simplemente tendremos que utilizar instrucciones print para realizar la exportación. En contrapartida, hace necesaria la instalación de Python en el sistema pues utiliza su módulo sys.
    Otra dificultad añadida aparece si el formato al que queremos exportar el fichero es binario. En este caso no podríamos utilizar esta forma de simplificación necesitando además el módulo struct que Python proporciona para esos menesteres.

    Los objetos vistos desde Blender
    No es objeto del presente trabajo hacer una pormenorizada explicación de los distintos módulos que provee Blender para la automatización de tareas. Para eso remitimos al lector a la documentación correspondiente. Sin embargo, no podremos avanzar mucho sin unas pocas palabras sobre el trabajo fundamental con algunos de estos módulos.

    El objetivo del script de ejemplo es la “pedagogía” más que la eficiencia o la exactitud, por lo que se ha intentado mantener en un mínimo razonable todo el aparato de importaciones y código extraño. Incluso el autor se ha reprimido del uso de la herencia a la hora de plantearlo. Por ello, sólo haremos hincapié en los puntos básicos. Uno de los cuales es el bucle de exportación, que en nuestro caso se encuentra dentro de la clase ExportadorEscena, concretamente ExportadorEscena.proceso().

    64 class ExportadorEscena:
    65 def __init__(self, nomFichero):
    66 self.fichero = nomFichero
    67
    68 def proceso(self):
    69 global losObjetos # Utilizo los objetos cargados
    en la lista global
    70 print "#VRML V2.0 utf8\n" # cabecera del fichero
    71 print "#%s" % self.fichero # El nombre del fichero
    72 for obj in losObjetos: # Proceso la lista de
    objetos
    73 tipo = obj.getType() # Obtener el tipo de
    objeto que vamos a tratar
    74 if tipo == 'Mesh':
    75 o = Objeto(obj)
    76 o.proceso()
    77 elif tipo == 'Camera':
    78 c = Cámara(obj)
    79 c.proceso()
    80 elif tipo == 'Lamp':
    81 luz = Luz(obj)
    82 luz.proceso()
    Lo primero que haremos en el proceso será escribir la cabecera del fichero (70) y añadimos un comentario al mismo con el nombre del fichero (71). Obsérvese que basta con escribir directamente en la salida estándar, puesto que la habíamos redireccionado a nuestro fichero. Después de esta cabecera comienza un bucle for que constituye el centro de todo el script.

    Dicho bloque (72-82) se encarga de procesar los objetos que se hayan incluido en la lista global losObjetos que vimos al principio. Puesto que en el fichero .blend que nos servirá de ejemplo sólo hay objetos de tipo Mesh, Camera y Lamp, el script sólo tiene en cuenta estos tipos. Se puede observar la similitud en la estructura de dicho proceso: primero se crea un objeto del tipo conveniente y a continuación se llama a su función miembro proceso(). Esta coincidencia esta llamada a ser tratada mediante herencia, sin embargo, se ha evitado llevarla a cabo en este caso para no confundir a aquellos que no estén acostumbrados a la programación orientada a objetos. La ventaja de dicha programación nos permitiría, por ejemplo, escribir sólo una vez el código que realizara el cambio de sistema de coordenadas en la clase base y que los demás objetos hijos lo heredaran.

    Los objetos desde VRML
    La estructura que siguen las clases dentro del script están acómodadas a la estructura que debemos generar para VRML. Es decir, los objetos tienen sus mallas definidas como listas de vértices que luego son referenciados en listas de caras. Dentro de esa estructura de sólido llevan también incluida una estructura de “apariencia”.

    En el caso de la apariencia, y con el objeto de abreviar un poco, sólo se han tenido en cuenta los parámetros más importantes del material y se han obviado otros como las texturas.

    El problema fundamental desde VRML era la representación de objetos y el cambio de origen para los puntos que definen la malla. Como sabemos, Blender almacena la información de las mallas en coordenadas locales. Es decir, las coordenadas de un punto están dadas teniendo como origen el “centro” del objeto al que pertenece. Para saber la posición real de un punto (desde el 0 absoluto) debemos tener en cuenta las coordenadas del punto respecto al centro del objeto y la posición, rotación y tamaño del objeto. Al pasar esta información a VRML nos damos cuenta que un sistema no se corresponde con el otro. Las transformaciónes en VRML se almacenan de forma distinta y en este sistema no es lo mismo rotar una figura y luego trasladarla, que trasladar una figura y luego rotarla, pues las transformaciónes se realizan todas desde el centro absoluto.

    Ante estas consideraciones, la salida más sencilla era exportar la malla de forma absoluta haciendo la conversión de “centros”.

    83 class Malla:
    84 ....
    85 def absoluto(self, v):
    86 vr = Mathutils.Vector([v[0], v[1], v[2]])
    87 vr.resize4D()
    88 return Mathutils.VecMultMat(vr, self.matriz)
    El método absoluto() se encarga de convertir una lista de tres valores en un objeto vector del módulo Mathutils para luego redimensionarlo y multiplicarlo por la matriz propia del objeto. Devuelve un vector con la trasformación realizada.

    Reutilizando cosas
    Otro de los aspectos a tener en cuenta cuando se realice una exportación es la posible reutilización de objetos, texturas, materiales... que pueda realizar el sistema al que estamos exportando. No sólo eso, sino también en qué manera lo vamos a hacer.

    En nuestro caso hemos decidido reutilizar los materiales. Para ello hemos habilitado un diccionario python donde almacenar los que vamos encontrando. Eso nos permite luego decidir si tenemos que hacer la definición completa o sólo una inclusión de la misma.

    89 materiales = {}
    90 class Material:
    91 def __init__(self, mat):
    92 global materiales
    93 self.nombre = mat.getName()
    94 if materiales.has_key(self.nombre):
    95 self.definido = 1
    96 else:
    97 self.definido = 0
    98 materiales[self.nombre] = 1
    99 self.color = mat.getRGBCol()
    100 self.ambiente = mat.getAmb()
    101 self.spec = mat.getSpecCol()
    102 self.emit = mat.getEmit()
    103 self.brillo = mat.hard/255.0
    104 self.transparencia = 1 - mat.alpha
    En el fragmento de script se puede destacar la definición de un diccionario python vacío llamado materiales (89). A continuación, dentro de la clase que va a procesar este tipo de objetos activamos definido como flag que nos dira si hay que hacer el proceso completo o no (94-97). Si no esta definido con anterioridad el constructor de la clase añade ese material al diccionario (98 y carga los valores necesarios para procesar el material (99-104).

    El script completo
    105 #!BPY
    106 """ Registramos el script en los menús de Blender
    107 Name: 'Ejemplo de la conferencia...'
    108 Blender: 23
    109 Group: 'Export'
    110 Submenu: 'Todos los objetos...' todos
    111 Submenu: 'Objetos seleccionados...' selec
    112 Tooltip: 'Esto es un ejemplo para la conferencia'
    113 """
    114
    115 import Blender
    116 import sys
    117 from Blender import NMesh, Lamp, Window
    118 from Blender import Scene, Mathutils
    119
    120 losObjetos = []
    121 materiales = {}
    122
    123 class Material:
    124 def __init__(self, mat):
    125 global materiales
    126 self.nombre = mat.getName()
    127 if materiales.has_key(self.nombre):
    128 self.definido = 1
    129 else:
    130 self.definido = 0
    131 materiales[self.nombre] = 1
    132 self.color = mat.getRGBCol()
    133 self.ambiente = mat.getAmb()
    134 self.spec = mat.getSpecCol()
    135 self.emit = mat.getEmit()
    136 self.brillo = mat.hard/255.0
    137 self.transparencia = 1 - mat.alpha
    138
    139 def proceso(self):
    140 if self.definido == 1:
    141 print "\t\t# Material:", self.nombre, "ya definido."
    142 self.escribeMaterialRepetido()
    143 else:
    144 print "\t\t# Definición del material:", self.nombre
    145 self.escribeMaterial()
    146
    147 def escribeMaterial(self):
    148 # Inicio la definición del material
    149 print "\t\tmaterial DEF %s Material {" % self.nombre
    150 print "\t\t\tdiffuseColor", self.color[0],
    self.color[1], self.color[2]
    151 print "\t\t\tambiéntIntensity", self.ambiente
    152 print "\t\t\tspecularColor", self.spec[0], self.spec[1],
    self.spec[2]
    153 print "\t\t\temissiveColor", self.emit, self.emit,
    self.emit
    154 print "\t\t\tshininess", self.brillo
    155 print "\t\t\ttransparency", self.transparencia
    156 # Cierro la definición del material
    157 print "\t\t}", "# Final del material", self.nombre
    158
    159 def escribeMaterialRepetido(self):
    160 print "\t\tmaterial USE", self.nombre
    161
    162 class Apariencia:
    163 def __init__(self, m):
    164 self.materiales = m
    165
    166 def proceso(self):
    167 # Cabezara de la apariencia
    168 print "\tappearance Appearance {"
    169 for m in self.materiales: #Proceso los materiales
    170 mat = Material(m)
    171 mat.proceso()

    172 # Cierre de la apariencia
    173 print "\t} # Fin apariencia"
    174
    175 class Malla:
    176 def __init__(self, m):
    177 self.malla = m.getData()
    178 self.nombre = m.name
    179 if (self.malla.getMode() & NMesh.Modes.TWOSIDED) == 0:
    180 self.doblecara = 1
    181 else:
    182 self.doblecara = 0
    183 self.verts = self.malla.verts
    184 self.faces = self.malla.faces
    185 self.matriz = m.matrixWorld
    186
    187 def proceso(self):
    188 # Inicio el proceso de la malla
    189 print "\tgeometry IndexedFaceSet {"
    190 # Una o dos caras
    191 if self.doblecara == 1:
    192 print "\t\tsolid FALSE # Dos caras"
    193 else:
    194 print "\t\tsolid TRUE # Una cara"
    195 self.vértices()
    196 self.caras()
    197 # Finalizo el proceso de la malla
    198 print "\t} # Fin de la geometría:", self.nombre
    199
    200 def vértices(self):
    201 print "\t\tcoord DEF c_%s Coordinate {" % self.nombre
    202 print "\t\t\tpoint ["
    203 for v in self.verts:
    204 vr = self.absoluto(v)
    205 print "\t\t\t\t %f %f %f," % (vr[0], vr[1], vr[2])
    206 print "\t\t\t] # Final de los vértices"
    207 print "\t\t} # Fin coordenadas c_%s" % self.nombre
    208
    209 def absoluto(self, v):
    210 vr = Mathutils.Vector([v[0], v[1], v[2]])
    211 vr.resize4D()
    212 return Mathutils.VecMultMat(vr, self.matriz)
    213
    214 def caras(self):
    215 print "\t\tcoordIndex ["
    216 for f in self.faces:
    217 verCara = ""
    218 for v in f.v:
    219 verCara += "%d, " % v.index
    220 print "\t\t\t%s-1," % verCara
    221 print "\t\t] # Final de la definición de las caras"
    222
    223 class Objeto:
    224 def __init__(self, obj):
    225 self.objeto = obj

    226 self.nombre = obj.name
    227 self.malla = obj.getData()
    228
    229 def proceso(self):
    230 print "# Esto es el objeto malla: %s" % self.nombre
    231 # Comienza el proceso
    232 print "DEF %s Shape {" % self.nombre
    233 # Obtengo los materiales
    234 mats = self.malla.materials
    235 # Proceso la "apariencia"
    236 ap = Apariencia(mats)
    237 ap.proceso()
    238 # Proceso la "malla"
    239 malla = Malla(self.objeto)
    240 malla.proceso()
    241 # Cierro la forma
    242 print "} # Shape %s" % self.nombre
    243
    244 class Luz:
    245 def __init__(self, l):
    246 self.luz = l
    247
    248 def proceso(self):
    249 print "# Aquí iría la definición de un punto de luz -"
    250 print "# Dejamos al lector el ejercicio de definir los
    distintos tipos de luces."
    251 print "# Recordarle, únicamente, la conveniencia de
    desactivar la luz 'subjetiva'"
    252 print "# que coloca VRML por defecto."
    253 print "# ------------------------------------"
    254
    255 class Cámara:
    256 def __init__(self, cam):
    257 self.cámara = cam
    258 self.nombre = cam.name
    259 self.pos = cam.getLocation()
    260 self.orien = cam.getEuler()
    261
    262 def proceso(self):
    263 print 'DEF %s Viewpoint {' % self.nombre
    264 print '\tdescription "%s"' % self.nombre
    265 # Hago trampas con el ángulo de rotación del vector de
    orientación y escablezco un
    266 # valor para evitar complicar más el script
    267 print '\torientation', self.orien[0], self.orien[1],
    self.orien[2], '1.5'
    268 print '\tposition %f %f %f' % (self.pos[0], self.pos[1],
    self.pos[2])
    269 print '} # Fin de la definición de', self.nombre
    270
    271 class ExportadorEscena:
    272 def __init__(self, nomFichero):
    273 self.fichero = nomFichero
    274
    275
    276 def proceso(self):
    277 global losObjetos #Objetos cargados en la lista global
    278 # escribo la cabecera del fichero
    279 print "#VRML V2.0 utf8\n"
    280 print "#%s" % self.fichero # El nombre del fichero
    281 for obj in losObjetos: # Proceso la lista de objetos
    282 tipo = obj.getType() # Obtener el tipo de objeto
    283 if tipo == 'Mesh': # y tratarlo
    284 o = Objeto(obj)
    285 o.proceso()
    286 elif tipo == 'Camera':
    287 c = Cámara(obj)
    288 c.proceso()
    289 elif tipo == 'Lamp':
    290 luz = Luz(obj)
    291 luz.proceso()
    292
    293 def fichero(nomFichero):
    294 if nomFichero.find('.wrl', -4) < 0:
    295 nomFichero += '.wrl'
    296 # Abro el fichero y lo conecto con std
    297 f = open(nomFichero, 'w')
    298 std = sys.stdout
    299 sys.stdout = f
    300 # Proceso los datos
    301 sce = ExportadorEscena(nomFichero)
    302 sce.proceso()
    303 # Devuelvo el control de stdout y cierro el fichero
    304 sys.stdout = std
    305 f.close()
    306
    307 #------------------------------------------------------
    308 # MAIN
    309 #------------------------------------------------------
    310 try:
    311 ARG = __script__['arg'] # Capturo argumento del submenú
    312 except:
    313 print "Versión antigua"
    314 ARG = 'selec'
    315
    316 if Blender.Get('versión') < 230:
    317 print "Aviso: ¡Versión incorrecta de blender!"
    318 print " No esta utilizando Blender 2.30 o superior, "
    319 print " descargue una versión más actualizada... "
    320 else:
    321 if ARG == 'todos':
    322 escena = Scene.getCurrent()
    323 losObjetos = escena.getChildren()
    324 else:
    325 losObjetos = Blender.Object.GetSelected()
    326
    327 Window.FileSelector(fichero, "Exportar")


    Última edición por 3dpoder; 10-02-2009 a las 00:34

  2. #17
    Administrador y fundador. Avatar de 3dpoder
    Fecha de ingreso
    Apr 2002
    Mensajes
    15,460

    Blender Extendiendo Blender: Programando Plugins de Texturas

    Extendiendo Blender: Programando Plugins de Texturas
    Guillermo Vaya



    Si el modelado es la parte que nos permite comprender la forma del objeto, las texturas son todo aquello que “da color” a los modelos, tanto en el sentido figurado como enel estricto. Son imágenes aplicadas a las caras de los modelos,las cuales definen o modifican las propiedades del material.

    Introducción a las Texturas y a las TexturasProcedurales
    Las texturas pueden tener entre una y tres dimensiones, según queramos que afecten de forma lineal, superficial o a todo un volumen. Sin embargo, lo más habitual suelen ser las texturas bidimensionales, ya que con éstas se puede modificar todo un modelo aplicandole las diversas caras.

    Los parámetros que puede modificar son muy diversos. Para mucha gente, las texturas únicamente asignan el color a las caras, pero esto no es así, sino que un buen texturizado puede hacer brillar lo que antes no era más que un mal modelo. Parámetros como la reflexión o la transparencia alpha son ejemplos de algunas características del material que una textura puede modificar para una correcta visualización del objeto. Mediante las texturas podemos simular elementos de modelado que no existen (“bumping map”, también llamado canal nor) o incluso generarlos (“displacement map”).

    Para poder conseguir nuestra biblioteca de texturas, hay que empezar distinguiéndolas en dos tipos:


    • Procedurales. Son aquellas generadas por un algoritmo y que se implementan gracias a la programación. Son calculadas en tiempo de renderizado.


    • No procedurales. Son dibujos, fotografías y, en definitiva, cualquier imagen estatica. No dependen de ningún parámetro y, por lo tanto, siempre quedan de la misma forma.

    Aunque aparentemente las no procedurales den la impresión de ser más fáciles de conseguir (mucha gente piensa que tomando una fotografía o haciendo un dibujo rápido se pueden generar buenas texturas), lo cierto es que tanto uno como otro modelo son bastante trabajosos a la hora de crearlos.

    Una textura procedural se consigue mediante la aplicación de modelos matematicos, es muy complicado generar un modelo matemático bueno que solucione el problema. Pero una vez esta desarrollado éste, el paso al algoritmo programado es bastante sencillo. En cambio, en las no procedurales pasa justo lo contrario: encontrar lo que necesitas es fácil, pero su conversión a una imagen de ordenador que no tenga parámetros de iluminación residuales es realmente complicado.

    Se ha de destacar que el principal problema y virtud de las texturas procedurales es su propiedad de mantener patrones repetitivos. Al aplicar una procedural como mapa de color, podemos ver cómo los caracteres predominantes se repiten de forma obvia, lo cual suele dar un desagradable efecto de irrealidad. Además, los patrones repetitivos son una de las causas más comunes de aliasing al alejarnos de la textura (o lo que es lo mismo, al reducirla).

    De igual manera es una característica indispensable, ya que de otra forma no podríamos predecir el resultado: imagina una textura que a cada render tomase una forma distinta; sería poco útil, ¿no? Incluso aquellas que nos parecen completamente aleatorias no lo son, y no sólo porque un ordenador no pueda generar un elemento aleatorio puro, sino porque ya hemos visto que no interesa.

    Todo esto nos lleva a pensar que las texturas procedurales son el aliado perfecto para generar modificaciones en los materiales.

    Consiguiendo bmake
    Los desarrolladores de Blender hacen todo lo posible por hacer más fácil la colaboración con los proyectos de la Blender Foundation. Un claro ejemplo es la herramienta bmake. bmake es un shell script creado para facilitar la tarea a aquellos interesados en desarrollar un plugin. Compilara nuestro código y generara una librería de enlace dinamico (generalmente caracterizada por la extensión .so en linux). De ésta manera los plugins que ejecutemos correran en Blender como si fuesen parte del programa original. Es por esto que hay que tener cierto cuidado al ejecutar plugins que no sepamos que son estables, cualquier error en el plugin podría estropear la ejecución de Blender y causar un cierre prematuro e indeseado de éste.

    Si bien hasta aquí es todo muy bonito, el futuro desarrollador de plugins puede encontrarse con un pequeño contratiempo. En los paquetes de ciertas distribuciones (como por ejemplo en Debian) la utilidad bmake y todo lo necesario para la compilación no esta empaquetado. Ésto puede ser debido a que realmente debería pertenecer a un futuro blender-dev.

    Para conseguir los ficheros necesarios bajamos una versión del cvs:

    # cvs -d:pserver:anonymous@cvs.blender.org:/cvsroot /bf-blender
    co blender
    y la compilamos

    # scons release
    La utilidad bmake estará en (path cvs)/blender/release/plugins/bmake y las librerías necesarias en (path cvs)/blender/source/blender/blenderpluginapi. Copiaremos bmake en el directorio de plugins y los headers los compiaremos bajo plugins, en el directorio include.

    En dicho directorio también hay otros dos llamados texture y secuencer. En texture será donde crearemos los ficheros .c con el código del plugin. ¡Ahora ya tenemos todo! Para compilar iremos a la carpeta textures y teclearemos:

    # ./bmake plugin.c
    Para probarlo en la carpeta textures debería haber dos ejemplos: tiles.c y clouds2.c

    # ./bmake tiles.c
    # ./bmake clouds2.c
    Cualquiera de los anteriores debería compilar sin problemas.

    Estructura de un plugin de texturas
    En la documentación de Blender.org viene una estructura básica para comenzar a diseñar un plugin. Aprovecharemos este recurso para evitar empezar de cero.

    # include "plugin.h
    Esta directiva de preprocesador ha de estar incluida en todo plugin que queramos desarrollar. Si no lo hacemos así, habrá varias constantes y funciones que no estarán disponibles y por tanto no se podrá compilar correctamente.

    char name[24] = ""; /* Texture name */
    Aquí ira el nombre de nuestro plugin tal y como queramos que aparezca en la interfaz de Blender, como se puede apreciar, tiene un máximo de 23 caracteres.

    #define NR_TYPES 3
    char stnames[NR_TYPES][16] = {"Intens", "Color", "Bump"};
    NR_TYPES da el número de subtipos que existiran en nuestro plugin. De esta manera se puede tener varias texturas procedurales de características parecidas bajo un único archivo de plugin, pudiendo cambiar de una a otra desde la selección que nos brinda la interfaz de Blender. Un ejemplo de esto es la textura de Clouds, podemos seleccionarlas en blanco y negro o en color. La siguiente línea indica los nombres que asignaremos a estos subtipos. No se debe superar los 16 caracteres para el nombre (no, ni siquiera modificando la declaración, si haces eso Blender puede dar resultados inesperados).

    VarStruct varstr[] = {
    {NUMIFLO, "Const 1", 1.7, -1.0, 1.0, ""},
    };
    typedef struct Cast {
    float a;
    } Cast;
    Estas dos estructuras son las que nos daran la información que le pidamos al usuario. Hay que tener en cuenta que han de estar ordenadas las variables de igual forma que los botones, para que el valor de uno sea asignado a la otra.

    varstr define los botones que se van a mostrar así como sus parámetros básicos. Se podrían dividir en dos grupos básicos:

    1. Numéricos: Esta dividida en 3 subtipos combinados con un flo (coma flotante) o con un int (para enteros) mediante un operador or ("|"):

    • num: valor numérico, se edita directamente.
    • numsli: barra deslizadora para elegir el valor.
    • tog: un interruptor, consta de dos posiciones.

    2. Caracteres: label, sirven para entradas de tipo texto.
    A continuación especificamos el nombre, que será lo que aparezca en el texto del botón. No debe superar los 15 caracteres.
    Para los elementos de tipo numérico, debemos especificar el valor por defecto, mínimo y máximo, en este orden. En el caso de los tog, el valor mínimo es el de pulsado y el máximo el de libre.
    Detras viene el “tip” o la ayuda que aparecerá cuando el puntero esté unos momentos encima del botón. El máximo es de 80 caracteres y si no se quiere usar, debemos dejar un string nulo ("").


    El Cast es la estructura con la cual se nos pasaran los valores tomados por la interfaz. Por cada elemento que hubiese en el varstr, aquí habrá otro, en el mismo orden y con el tipo apropiado. Generalmente se suele usar el mismo nombre que para el botón, aunque esto último no es necesario (es una practica recomendable, ya que mejora la legibilidad de nuestros programas y acelera la depuración de los mismos).

    float result[8];
    Aquí ira lo que queramos devolver a Blender. No tiene porqué estar relleno entero, sino que dependerá del valor que demos a la sentencia return al final de la función teXDoit.

    result[0]: intensidad. Éste es el único que hay que rellenar siempre.
    ● result[1]: valor de rojo.
    ● result[2]: valor de verde.
    ● result[3]: valor de azul.
    ● result[4]: valor de alfa
    ● result[5]: desplazamiento de la normal en el eje X.
    ● result[6]: desplazamiento de la normal en el eje Y.
    ● result[7]: desplazameinto de la normal en el eje Z.

    float cfra;
    Esta variable nos la establece blender con el frame actual.

    int plugin_tex_doit(int, Cast*, float*, float*, float*);
    Prototipo de la función principal para registrarla en la interfaz de blender. Más adelante hablaremos de sus parámetros.

    /* Fixed Functions */
    int plugin_tex_getversión(void) {
    return B_PLUGIN_VERSION;
    }
    void plugin_but_changed(int but) { }
    Esta parte del código no hace falta tocarla, y no deberemos tocarla a mnos que sepamos lo que estamos haciendo. Aun así, deberemos añadirla por compatibilidad de código.

    void plugin_init(void) { }
    Esta función sólo la modificaremos si hace falta realizar algún tipo de calculo o inicialización antes de comenzar con el procedimiento. Es importante tener en cuenta que si este plugin se usa en más de un sitio, la función init será llamada cada vez que se acceda a uno de estos elementos, por lo que no se debe inicializar elementos de forma global a menos que estemos muy seguros de que es eso lo que queremos.

    void plugin_getinfo(PluginInfo *info) {
    info->name=name;
    info->stypes=NR_TYPES;
    info->nvars=sizeof(varstr)/sizeof(VarStruct);
    info->snames=stnames[0];
    info->result=result;
    info->varstr=varstr;
    info->init=plugin_init;
    info->tex_doit=(TeXDoit) plugin_tex_doit;
    info->callback=plugin_but_changed;
    Esta función es la que se encarga de registrar los nombres de los distintos elementos de comunicación entre Blender y el plugin. Si cambias algún nombre más arriba, entonces también deberas cambiarlo aquí. Aunque lo más recomendable sería no cambiar ninguno de los nombres clave para que la gente pueda entender mejor el código.

    int plugin_tex_doit(int stype, Cast *cast, float *texvec,
    float *dxt, float *dyt)
    Cabecera de la función principal. Cuando Blender quiera ejecutar el plugin, ésta será la función a la que llame (después de haber llamado a la de init si es la primera vez que accede al plugin).
    Parámetros:

    • stype: indica el subtipo que seleccionó el usuario, en el caso de haber más de uno.


    • cast: estructura con los datos de la selección de parámetros del usuario a través de la interfaz de botones que le dimos.


    • texvec: array de tres elementos, indicando la posición x, y, z del texel (el punto de la textura).


    • dxt y dyt: estos valores estarán a null a menos que se haya pedido antialiasing (osa). En caso de contener algún valor, indicaran el tamaño de la textura en espacio de pixel (la explicación de este concepto queda fuera de este manual, consulta algún libro de texturas procedurales para saber lo que es).

    if (stype == 1) {
    return 1;
    } if (stype == 2) {
    return 2;
    }
    return 0;
    De todo lo anterior sólo explicaremos el valor de return. Según
    devolvamos 0, 1, 2 ó 3, Blender accedera a distintos valores del array result.

    • 0: únicamente mirara la intensidad.


    • 1: mirara tanto la intensidad como los valores de color rgba.


    • 2: usara los valores de intensidad y de mapa de desplazamiento.


    • 3: accedera a todo el array.


    Ejemplo: Smiley.c
    Ahora comenzaremos a desarrollar un sencillo plugin. Digo sencillo ya que no tiene ni siquiera antialiasing, sólo con este tema ya podría constituir por sí solo un tema entero debido a la cantidad de posibilidades que tiene así como la elección de cual es mejor o peor según qué casos.

    El plugin generara un “smiley” ligeramente configurable, podremos cambiar el tamaño así como si esta triste o alegre (menuda chorrada, ¿no?). Con este plugin quiero demostrar que mediante procedurales se pueden realizar múltiples texturas, incluso aquellas más inveros?miles, aunque a veces no sean las más útiles. Además nos servirán como ejemplo para introducirnos un poco más en el mundo de las texturas procedurales de Blender.

    Empezamos poniendo las librerías en las cuales se va a apoyar nuestro plugin. Como untilizaremos algunas funciones matemáticas avanzadas (seno, coseno y potencia) deberemos incluir la librería math. Además añadiremos la definición de plugin para que use los parámetros, funciones y constantes necesarias para la api de plugin.

    #include <math.h>
    #include "plugin.h"
    Ahora le daremos un nombre, ¿qué mejor que smiley si eso es lo que es? Es conveniente siempre dar nombres que aseguren al usuario la comprensión de lo que hace el plugin a la vez que son cortos.


    char name[] == "smiley";
    Dado que esto es un ejemplo sencillo y no queremos complicarnos la vida, usaremos un único subtipo de plugin. Aquí podríamos haber metido tantos como posibles caras queramos tener, pero lo dejaremos en sólo uno. Le asignaremos el nombre smiley ya que no v a haber más tipos.


    #define NR_TYPES 1
    char stnames [NR_TYPES][16] = {"smiley"};
    Ahora definimos los parámetros que queremos que sean modificables por el usuario, así como su presentación en la interfaz de Blender. En nuestro caso sólo queremos que pueda jugar con dos de ellos: el radio del smiley y la boca. Usaremos un deslizador (“slider” en inglés) para variar esos tipos. Se me ocurre que también podríamos haber usado un interruptor (“toggle”) para la boca, pero con un deslizador podemos dar un mayor rango de valores.

    Para el radio elegimos un valor de 0 a 1, con 0 no habrá cara, mientras que con 1 ésta ocupara todo. Para la boca pondremos valores más pequeños, ya que los usaremos a modo de tanto por ciento e incluímos valores negativos y positivos para que exista tanto una cara sonriente como una cara triste.

    VarStruct varstr[] = {
    NUMSLI|FLO, "radio", 1.0, 0.0, 1.0, "radio de la cara",
    NUMSLI|FLI, "boca", 0.25, -0.25, 0.25,
    "altura del centro de la boca"
    };
    Ahora hay que decirle a Blender con qué variables se corresponderan en nuestro código. Aprovecho para recordar que deberemos pasarselas en el mismo orden que en la definición de la interfaz par que blender no se lie.

    typedef struct Cast
    {
    float rCara;
    float aboca;
    } Cast;
    Las dos variables siguientes las definimos para la compatibilidad con el sistema de plugins de Blender. En result es donde se almacenaran los datos a devolver. Nosotros sólo usaremos los de intensidad y color, pero aun así hay que usar un array de 8 elementos. cfra lo definimos pero no lo usaremos (es decir se quedará con valor 0).

    float result[8];
    float cfra;
    La siguiente parte es la parte que no vamos a tocas para nada. Ya que no nos sirve de mucho.

    int plugin_tex_doit (int, Cast*, float*, float*, float*);
    int plugin_tex_getversión (void)
    {
    return B_PLUGIN_VERSION;
    }
    void plugin_but_changed(int but)
    {
    }
    Ahora toca rellenar la función de inicialización del plugin. Como nuestra cara no necesita tener ningún valor inicializado, dejaremos la función en blanco.

    void plugin_init(void)
    {
    }
    La siguiente función es bastante importante, ya que indica a Blender la manera de comunicarse. Si bien hemos usado los nombres que venían por defecto en la estructura, aquí es donde habría que decirle si hemos usado otros nombres en vez de esos. Personalmente recomiendo seguir usando estos nombres, ya que así se hace más legilble para futuros desarrolladores formando una especie de estándar consensuado.

    void plugin_getinfo(PluginInfo *info)
    {
    info->name=name;
    info->stypes= NR_TYPES;
    info->nvars= sizeof(varstr)/sizeof(VarStruct);
    info->snames= stnames[0];
    info->result= result;
    info->cfra= &cfra;
    info->varstr=varstr;
    info->init= plugin_init;
    info->tex_doit= (TeXDoit) plugin_tex_doit;
    info->callbak = plugin_but_changed;
    }
    El siguiente trozo de código es el corazón y cerebro de nuestro plugin. Es el que hará todos los cálculos y llamara a las funciones necesarias. He de comentar que tiene un grave problema de diseño, para aquellos que ya hayan desarrollado más cosas con anterioridad verán que este pseudomain no es modular, sino que realiza él solito y por sí mismo todos los cálculos. Cuan hubiera estado mejor separar el código en trozos más pequeños, invitando de esta manera a futuros desarrolladores a hacer pequeñas modificaciones y hacerles la vida más cómoda posible. Pero como esto no es un plugin pensado para su uso he preferido omitir el usar algunas funciones típicas de los lenguajes de shading así como el dividir la estructura en trozos. Otra de las razones por las que recomendaría usar la estructura en trozos es para poder utilizar el “antialiasing” de forma cómoda. De todas formas iremos analizandola poco a poco par que quede todo claro.

    Primero usaremos la definición anterior del “main” del plugin y definiremos las variables que vayamos a usar así como su inicialización en caso de ser necesaria.

    int plugin_tex_doit(int stype, Cast *cast, float *texvec, float
    *dxt, float *dyt)
    float cox, coy, cara, boca, rboca;
    cara=cast->rCara;
    boca=cast->aboca;
    result[0]=1.0; //precolocamos la intensidad
    result[4]=1.0; //evitamos transparencias
    cox=texvec[0];
    coy=texvec[1];
    Usando la fórmula básica del círculo comprobamos si el texel a tratar esta dentro o fuera.

    if ((pow(cox,2) + pow(coy,2))<pow(cara,2)) /* dentro de la cara */
    He decidido dividir la cara en cuatro cuadrantes, para ello primero lo divido en dos mitades

    if (texvec[1]>=0.0) /* mitad superior */
    Movemos la altura del pixel a un supuesto centro a la altura de los ojos.

    coy=texvec[1]-(0.5*cara);
    Ahora miramos en el lado al que esta y modificamos la posición para hacerla relativa al centro del ojo en la coordenada X según corresponda.

    if (texvec[0] <= 0.0) /* cuarto superior izquierdo */
    cox = texvec[0] + (0.5*cara); /* centro ojo izdo. */
    else
    cox = texvec[0] - (0.5*cara); /* centro ojo dcho. */
    Con la misma fórmula del círculo, comprobamos si se esta mirando dentro del ojo o fuera de él. Si estamos dentro del ojo, asignaremos un color negro y si estamos fuera, amarillo.

    if((pow(cox,2) + pow(coy,2)) < pow((0.25*cara),2)) /*dentro*/
    {
    result[1]=0.0; /*texel negro*/
    result[2]=0.0;
    result[3]=0.0;
    }
    else /*fuera del ojo*/
    {
    result [1]=1.0; /*texel amarillo*/
    result [2]=1.0;
    result [3]=0.0;
    }
    Si no Estábamos en la mitad superior, quiere decir que estamos en la inferior y por tanto sólo hemos de preocuparnos de la boca. Comprobamos que la posición pertenezca a la zona de la boca, ya que si no el color a tomar es bastante claro.

    else /*mitad inferior*/
    {
    if ((texvec[0] >= -0.5*cara) && (texvec[0] <= 0.5*cara) &&
    (((texvec[1] >= -0.5*cara) && (boca >=0)) ||
    ((texvec[1]<= -0.5*cara)&&(boca<=0))))
    ...
    }
    Igual que antes modificamos las coordenadas para ponerlas en base al centro de la circunferencia que será la boca.

    coy = texvec[1] + 0.5*cara + 0.25*boca;
    cox = texvec[0];
    Ahora ya solo queda por saber si estaremos dentro o fuera de ella. Como antes, si estamos dentro asignaremos un color negro y si estamos fuera, será amarillo.

    if ((pow(cox,2) + pow(coy,2)) <= pow(boca*cara,2))
    {
    result[1]=0.0;
    result[2]=0.0;
    result[3]=0.0;
    }
    else
    {
    result[1]=1.0;
    result[2]=1.0;
    result[3]=0.0;
    }
    Si no estaba dentro del círculo de la cara, entonces pondremos el color como transparente, para que se vea el color original del material.

    else
    {
    result[4]=0.0; /* alfa al mínimo */
    result[1]=0.0;
    result[2]=0.5;
    result[3]=0.0;
    }
    Terminamos y salimos devolviendo el valor que indica que le
    pasaremos los valores de intensidad y color.

    return 1;
    Como se puede apreciar, este plugin es únicamente para un primer contacto con la interfaz de plugins, podría ser mejorable de muy diversas formas:

    • Permitir la personalización de los colores o incluso admitir mezclas.


    • Utilizar splines para la definición de la boca permitiendo así una mayor variedad de formas de la boca.
    • Antialiasing.


    • Soporte de canales nor o displacement maps.
    • Utilización de otras primitivas para la definición de los elementos.


    • Otros...


    Notas y más información
    Cosas a tener en cuenta:

    • Las coordenadas UVW usan un rango de coordenadas "[-1,1]".


    • Es preferible estar abriendo y cerrando Blender, ya que si no, puede no cargar bien.


    • Conviene tener un .blend con el plugin cargado. De esta forma se agiliza la visualización de los resultados si se esta abriendo y cerrarndo Blender.


    • Al asignar un color no debemos olvidarnos del canal alfa, ya que en C las variables se inicializan a 0 y por tanto no veríamos más que el color base del material del objeto.

    La compilación de plugins en Windows, se hace de manera distinta, ya que la herramienta bmake se basa en el gcc de linux (el cual no esta disponible en windows). En cambio los comandos a ejecutar seran, aun cuando no es seguro que funcionen al "100%".

    cd c:\blender\plugins\texture\sinus
    lcc -lc:\blender\plugins\include sinus.c
    lcclnque -DLL sinus.obj c:\blender\plugins\include\tex.def
    implib sinus.dll
    Algunos de los sitios de donde he sacado información y ejemplos de plugins:

    http://download.blender.org/document...lI/c11605.html
    pagina de la documentación oficial de Blender.

    http://www.cs.umn.edu/~mein/blender/plugins página con ejemplos y documentación de plugins.

    El código del plugin completo

    1 #include <math.h>
    2 #include "plugin.h"
    34
    char name [] = "smiley";
    5
    6 #define NR_TYPES 1
    78
    char stnames [NR_TYPES][16] = {"smiley"};
    9
    10 VarStruct varstr[]={
    11 NUMSLI|FLO, "radio", 1.0, 0.0, 1.0, "radio de la cara",
    12 NUMSLI|FLO, "boca", 0.25, -0.25, 0.25, "altura del centro
    de la boca"};
    13
    14 typedef struct Cast
    15 {
    16 float rCara;
    17 float aboca;
    18 } Cast;
    19
    20 float result[8];
    21 float cfra;
    22
    23 /***********Parte fija *********/
    24 int plugin_tex_doit (int, Cast*, float*, float*, float*);
    25 int plugin_tex_getversión (void)
    26 {
    27 return B_PLUGIN_VERSION;
    28 }
    29
    30 void plugin_but_changed(int but)
    31 {
    32 }
    33
    34 void plugin_init(void)
    35 {
    36 }
    37
    38 void plugin_getinfo(PluginInfo *info)
    39 {
    40 info->name=name;
    41 info->stypes= NR_TYPES;
    42 info->nvars= sizeof(varstr)/sizeof(VarStruct);
    43 info->snames= stnames[0];
    44 info->result= result;
    45 info->cfra= &cfra;
    46 info->varstr=varstr;
    47 info->init= plugin_init;
    48 info->tex_doit= (TeXDoit) plugin_tex_doit;
    49 info->callbak = plugin_but_changed;
    50 }
    51 /***********Fin parte fija *********/
    52
    53 int plugin_tex_doit(int stype, Cast *cast, float *texvec,
    float *dxt, float *dyt)
    54 {
    55 float cox, coy, cara, boca, rboca;
    56
    57 cara=cast->rCara;
    58 boca=cast->aboca;
    59 result[0]=1.0; //precolocamos la intensidad
    60 result[4]=1.0; //evitamos transparencias
    61 cox=texvec[0];
    62 coy=texvec[1];
    63
    64 if ((pow(cox,2) + pow(coy,2))<pow(cara,2))
    /*dentro de la cara*/
    65 {
    66 if(texvec[1]>=0.0) /*mitad superior*/ {
    67 coy=texvec[1] - (0.5*cara);
    68 if (texvec[0] <= 0.0) /*cuarto superior izquierdo*/
    69 cox=texvec[0] + (0.5*cara); /*centro ojo izdo.*/
    70 else
    71 cox=texvec[0] - (0.5*cara); /*centro ojo dcho.*/
    72 if((pow(cox,2) + pow(coy,2)) < pow((0.25*cara),2))
    /*dentro del ojo*/ {
    73 result[1]=0.0; /*texel negro*/
    74 result[2]=0.0;
    75 result[3]=0.0;
    76 }
    77 else /*fuera del ojo*/ {
    78 result [1]=1.0; /*texel amarillo*/
    79 result [2]=1.0;
    80 result [3]=0.0;
    81 }
    82 }
    83 else /*mitad inferior*/ {
    84 if ((texvec[0] >= -0.5*cara) && (texvec[0]
    <= 0.5*cara) &&
    85 (((texvec[1] >= -0.5*cara) && (boca >=0)) ||
    86 ((texvec[1]<= -0.5*cara)&&(boca<=0)))) {
    87 coy = texvec[1] + 0.5*cara + 0.25*boca;
    88 cox = texvec[0];
    89 if ((pow(cox,2) + pow(coy,2)) <=
    pow(boca*cara,2)) {
    90 result[1]=0.0;
    91 result[2]=0.0;
    92 result[3]=0.0;
    93 }
    94 else {
    95 result[1]=1.0;
    96 result[2]=1.0;
    97 result[3]=0.0;
    98 }
    99 }
    100
    101 else {
    102 result[1]=1.0;
    103 result[2]=1.0;
    104 result[3]=0.0;
    105 }
    106 }
    107 }
    108 else {
    109 result[4]=0.0;/*alfa al minimo*/
    110 result[1]=0.0;
    111 result[2]=0.5;
    112 result[3]=0.0;
    113 }
    114 return 1;
    115 }

    Última edición por 3dpoder; 10-02-2009 a las 01:53

  3. #18
    Administrador y fundador. Avatar de 3dpoder
    Fecha de ingreso
    Apr 2002
    Mensajes
    15,460

    Blender Rediseño de Yafray: Parte 1. La luz

    Rediseño de Yafray: Parte 1. La luz
    Alejandro Conty Estévez
    aconty@gmail.com ::


    Len la escena llamados luces. La afirmación parece trivial, pero la singularidad reside en que no todos estos objetos se corresponden con lo que intuitivamente llamaríamos luz.

    Estado actual
    Antes de entrar en detalles, veamos el diseño de estos elementos. Cada luz en yafray consiste en un objeto abstracto capaz de iluminar un punto de la escena. Por ello, definimos un método virtual llamado illuminate que toma como argumento un punto de una superficie y devuelve el color reflejado en una dirección concreta.

    Para devolver el color reflejado, es necesario que la luz llame al shader (material) asignado a esa superficie. Éste es uno de los puntos confusos del diseño sobre el que la gente a menudo pregunta. Parece poco intuitivo que la llamada a los shaders se delegue en las luces, por lo que volveremos sobre este punto más adelante. Por lo demás, con este interfaz, la lógica del cómputo de iluminación queda oculta tras esos objetos.







    De esta forma podíamos separar el núcleo del render del calculo de la iluminación. Definíamos la implementación de los distintos tipos de luces en ficheros separados sin que hubiera más dependencia que el citado interfaz. En una fase posterior del desarrollo, estos tipos de luces fueron separados aún más lejos con un sistema de plugins. Al ser tipos abstractos, sólo había que preocuparse de la creación de las instancias. A partir de ese punto, el objeto se manejaba de forma anónima y transparente.

    Para la gestión de la creación de objetos se usaron factorías abstractas (ver patrón factory). El entorno de la escena disponía de una tabla de funciones de construcción indexadas por el tipo del objeto. No sólo para las luces, también para los shaders y fondos de escena. Éstas funciones factoría se diseñaron con un prototipo como el siguiente:

    1 light_t * light_factory_t(paramMap_t &params,
    renderEnvironment_t &env);
    Donde el parámetro “params” guardaba toda la configuración del objeto a crear. Todas estas factorías se indexaron en una tabla por nombre definida de la siguiente manera:

    2 std::map<std::string,light_factory_t *> light_factories;
    De la misma forma se hizo para las texturas, shaders, etc ... Cuando un plugin se carga, se llama a una función predefinida que éste ha de contener y que se encarga de registrar las factorías necesarias para todo lo que éste contiene. Veamos un ejemplo de código del plugin con los shaders básicos:

    3 void registerPlugin(renderEnvironment_t &render)
    4 {
    5 render.registerFactory("generic",
    genericShader_t::factory);
    6 render.registerFactory("constant",
    constantShader_t::factory);
    7 std::cout<<"Registered basicshaders\n";
    8 }
    Aquí vemos como se registran las factorías para los tipos de shader “generic” y “constant”. A partir de este momento, cuando el cargador encuentra una sentencia xml que ordena la creación un shader del tipo “generic” busca la factoría en la tabla y la invoca dándole todos los parámetros de la sentencia xml. Como resultado devuelve un puntero a un shader que se almacena en la escena para posterior uso.

    Volviéndonos a centrar en las luces, todo este sistema permitió crear de manera independiente los tipos básicos de fuentes de luz sin tocar el núcleo. En general es un sistema bueno que sigue funcionando bien para los shaders. En las luces en cambio, no todo resultó tan bien. El problema fue que la definición de luz era demasiado amplia. Se entendía como luz cualquier cosa capaz de calcular la energía reflejada en una dirección sobre un punto dado. Debido a esto, caímos en el error de implementar métodos de iluminación global como luces en la escena. Éste es un caso claro de exceso de modularización, y es de estudio recomendado para todo el que se proponga diseñar un programa de envergadura.

    Los dos primeros casos fueron la hemilight y la photonlight. La primera calculaba la luz proveniente del fondo de la escena y la segunda las caústicas proyectadas desde un punto cualquiera de luz.

    En ese momento, el concepto de luz se disolvió. Las luces pasaron a ser métodos de iluminación. Esto no hubiera sido mayor problema de no ser por la complejidad que alcanzaron ciertos métodos de iluminación. En concreto la “luz” pathLight usa complejos cálculos de iluminación indirecta, sistema de caché e incluso cáusticas. Para ello llega a usar incluso mapas de fotones que son creados por otra “luz” independiente. Esto crea una serie de dependencias molestas entre varias luces y el núcleo del render.





    Como se indica en el gráfico, un supuesto módulo de subsurface scattering introduciría aún más dependencias. La única forma de evitarlo sería meterlo directamente dentro de la iluminación global, haciendo crecer la pathLight aún más. Ninguna de las dos opciones es muy recomendable.

    Valoración del estado actual
    Ante esta situación se pueden hacer las siguientes observaciones:

    • Los plugins pierden sentido en el momento que aparecen dependencias camufladas y colisiones.


    • El interfaz limitado con los plugins nos para de mejorar ciertos algoritmos y de añadir nuevas características.


    El primer error viene de mezclar los conceptos de “fuente de luz” y “método de iluminación”. Las fuentes de luz son posiciones, superficies o volúmenes que emiten energía. ¿Qué se espera de ellasí Teniendo en cuenta los algoritmos usados en yafray, lo que se necesita de una fuente de luz se puede resumir en:

    • Muestrear la fuente desde un punto cualquiera.


    • Muestrear la fuente en cualquier punto de ésta y en cualquier dirección.


    La primera de las funciones esta pensada para el raytracing desde el punto de vista hacia las fuentes de luz, también conocido como raytracing estándar. En este proceso, para calcular la energía que llega a un punto se usan técnicas de montecarlo en las que se muestrea la fuente de luz para cada punto visible desde la cámara. Las luces puntuales son un caso particular de superficie nula en las que sólo una muestra es necesaria. En general, asumimos que todas las fuentes de luz son muestreables.

    El segundo requisito es para poder llevar a cabo el photon mapping. Cuando hacemos esto recorremos el camino inverso, desde las luces hacia la escena. Por eso necesitamos una forma de obtener direcciones de disparo de fotones.

    Interacción luz-shader
    Al principio del desarrollo de yafray se partía del principio de que una fuente no puntual se muestreaba en diferentes direcciones y se llamaba al shader por cada una de ellas. Esto se debe a que originalmente, el resultado del shader depende no sólo de la energía que llega sino también de la dirección.

    Uno de los típicos efectos que se consiguen con este sistema son las “specular highlights”. Un “fake” de una reflexión borrosa de una luz puntual. Sin embargo, actualmente en yafray este método no se usa siempre. Por ejemplo en la iluminación global, donde el entorno se muestrea en la semiesfera tangente a la superficie. En lugar de llamar al shader para cada muestra, se calcula la energía total por separado y luego se llama al shader. Esto evita hacer tantas llamadas, y el resultado es lo suficientemente bueno como para que nadie se haya quejado aún.

    Lo interesante es que si seguimos este principio de calcular la energía por separado y luego llamar al shader sin dirección alguna, el diseño se simplifica. Quitamos libertad al shader para hacer ciertos efectos. Pero dado que las reflexiones se calculan en yafray por separado, el único efecto comúnmente usado que perdemos es el de los brillos especulares de luces puntuales. No hay manera de simular este efecto por ninguna otra vía, así que trataremos de poner algún añadido al diseño para conservarlas.

    Propuesta de diseño para fuentes de luz
    Siguiendo la directiva anteriormente descrita de calcular la energía por separado y asumir que el shader no tendrá la libertad de hacer ningún otro tipo de calculo experimental, el interfaz de una fuente de luz se reduce a calcular la energía que llega a un determinado punto.

    Con esto nos ahorramos el hacer un sistema de muestreo de fuentes de luz. Aún nos queda otro, el del mapeo de fotones, del que no podremos deshacernos. Pero ahora en vez de lanzar rayos en el núcleo en dirección a las fuentes, dejamos que sea la misma fuente la que haga el calculo que tenga que hacer.

    Para el caso de luces puntuales para las que pueda ser interesante un calculo de luz especular, pondremos un método adicional que nos permita conservar dicha característica.

    El primer método proporciona la manera estándar de calcular la cantidad de luz que llega a una superficie. Los parámetros son:

    • P: El punto en cuestión.


    • N: La normal al plano tangente al punto.


    • NU y NV: Los dos vectores ortogonales que definen el plano tangente.


    • scene: La escena.


    El segundo de los métodos es un método Ad--hoc para las luces puntuales que puedan provocar reflejos especulares de ellas mismas. En el caso de luces no puntuales, la manera correcta de calcularlo es con una reflexión borrosa que se vera en una fase posterior de diseño.



    Evidentemente, esta decisión de diseño que limita la libertad de un shader tiene un propósito. El método “incomingEnergy” encapsula toda la técnica montecarlo, y podemos aplicar ahí sin que afecte al resto una técnica de aceleración vía hardware gráfico. A continuación veremos cuales son las ideas detrás de este concepto.

    Iluminación montecarlo vía GPU
    Existen varias formas en las que podemos aprovechar el hardware gráfico para acelerar el render. Recordemos que hay una fase de “final gather” en la que para cada punto de la escena visible disparamos rayos en la semiesfera tangente. Hacemos esto para simular una integral de toda la luz que llega a ese punto. Al fin y al cabo no deja de ser un render estocastico del entorno del punto que vamos a sombrear.

    El propósito de este proceso es el de sumar toda la luz entrante. No es por lo tanto necesario que la calidad de ese render local sea muy alta. En realidad lo único importante es que el promedio de energía vista se aproxime a la real. Como es de esperar, para hacer un buen promedio es necesario lanzar bastantes rayos ralentizando el render.

    Ahora supongamos que disponemos de una forma de renderizar una apróximación de la escena usando openGL por ejemplo. Se entiende que esta apróximación usa la iluminación del mapa de fotones mediante algún truco como texturas o colores en los vértices. Si disponemos de esta característica, en lugar de lanzar todos esos rayos en la semiesfera tangente, podemos simplemente hacer un render a baja resolución usando openGL.

    La idea es renderizar con un ángulo muy abierto que aproxime la visibilidad de la semiesfera. Supongamos que hacemos un render de 100x100 pixels y luego hacemos la suma ponderada de esos pixels. Sería lo equivalente a tomar 10000 muestras por montecarlo estándar. Sólo que el hardware lo haría muchísimo más rápido. Teniendo en cuenta que el máximo razonable de muestras en yafray viene a ser unas 2000 ya que a partir de ahí empieza a ser excesivamente lento, el poder tomar 10000 muestras de golpe es sin duda un empujón bastante grande.

    Además, no sólo este proceso puede beneficiarse del hardware, también el render de reflexiones borrosas, luces de área y cualquier otra cosa que pueda funcionar con una apróximación de la escena. En estos casos particulares lo que se hará es renderizar con un cono pequeño en lugar de usar uno de 180 grados.



    Como se ve en el diagrama, siempre podemos tener ambas alternativas presentes. El render apróximado por hardware o el de calidad. Podemos elegir uno u otro a voluntad según los requisitos de calidad.

    Tareas a completar
    Para poder llegar a esto es necesario resolver los siguientes problemas:

    • Recorrer el árbol BSP con un cono en lugar de un rayo.


    • Poder renderizar en openGL con un cono de 180 grados sabiendo que ángulo corresponde a cada pixel del resultado.


    • Poder renderizar triangulos con la información de color del mapa de fotones.

    El último problema podemos aplazarlo ya que podemos avanzar dando pasos previos con sistemas de iluminación menos ambiciosos como hace la “hemiLight”.

    Para evitar volcar todos los triangulos de la escena en cada uno de esos renders locales, vendría bien poder escoger aquellos que caigan dentro del cono en el que vamos a renderizar. Hasta ahora cuando trazabamos un rayo, recorríamos los triangulos de la escena usando el árbol binario BSP. Eso evita tener que comprobarlos todos. Ahora queremos hacer lo mismo pero usando un cono en lugar de un rayo.

    Afortunadamente este algoritmo es bastante sencillo y se puede alcanzar con algunas modificaciones sobre el algoritmo original para rayos. Añadiendo este método extra de recorrido al árbol ya tenemos una forma sencilla de esquivar los triangulos que no nos interesan.

    En cuanto al segundo problema es sólo una cuestión de estudiar la proyección usada en openGL o el sistema que vayamos a usar. Para separar la interacción con el API de openGL, crearemos una clase de driver gráfico. En lugar de usar openGL directamente para el renderizado, lo haremos a través de este interfaz. En la siguiente sección veremos como podría ser.

    Driver GPU
    La funcionalidad que esperamos del hardware gráfico no es mucha. Simplemente querremos dibujar triangulos con un color o textura opcionalmente. El render se hará siempre sobre un buffer arbitrario. Veamos el interfaz propuesto:





    La primera función, “renderCone” se llamara al principio del render local para elegir el cono de visión y el buffer donde se pondrá el resultado. Despúes se podrán usar el resto de funciones para pintar triangulos con un color plano o con una textura. Para las texturas, será responsabilidad del driver el mantener una tabla de correspondencias entre las texturas que el llamante esta usando y las que vaya creando en el espacio del hardware gráfico.

    Como vemos, se define un interfaz genérico de render simple del que luego derivamos a la implementación específica de openGL. Seguramente no usaremos otra cosa para renderizar vía hardware, pero de esta forma centralizamos el uso del API y descontaminamos el resto del código.

    Existe un problema adicional en el uso del hardware. Yafray contempla la posibilidad de renderizar con hilos en varias CPU's. Hay que evitar que varios hilos accedan al hardware de manera concurrente. Afortunadamente openGL trabaja con contextos que se pueden intercambiar.

    En yafray, cada hilo de renderizado posee un contexto que nada tiene que ver con el de openGL. En este contexto se guarda información de interés que es específica de cada hilo, como:

    • Último objeto intersecado.


    • Profundidad de render.


    • Contribución actual al pixel que se esta renderizando.


    • etc ...


    Lo mejor que podemos hacer es introducir un driver GPU en cada uno de estos contextos. De esta forma no se pisaran unos hilos a otros. Suponemos que el mismo driver se ocupa de crear su propio contexto openGL.

    ¿Por dónde empezar?
    Antes de meternos directamente con la iluminación global total por hardware, conviene abordar problemas relajados relacionados con la misma. Si conseguimos hacer que funcione para los siguientes casos ya tenemos una gran parte del problema resuelta.

    GPU hemilight
    Veamos como podríamos calcular la iluminación que llega desde el fondo de la escena usando la GPU. El método de funcionamiento de la hemilight consiste en disparar rayos en una semiesfera tangente al punto de intersección. Si estos rayos golpean algún objeto se asume que son rayos de sombra y su contribución es nula. De otra forma alcanzan el fondo y los contamos como una contribución de luz positiva. Usamos el color del cielo como valor de contribución. Ya que estos son los primeros pasos, asumiremos que el color del cielo es constante. Más adelante se puede contemplar como hacer que podamos tener una textura de cielo o incluso un mapa HDR.

    Para que una escena de cielo abierto tenga una acabado de calidad aceptable es necesario lanzar un número de muestras alrededor de 200. Si bajamos este número empieza a aparecer ruido en torno a las sombras. Evidentemente este método es costoso. Lo que se propone aquí es evitar ese proceso de lanzar 200 rayos. Para ello podemos intentar renderizar una apróximación del cielo visible desde dicho punto con la GPU. Dado que la contribución de los objetos a la luz es completamente nula, el proceso es sencillo. Veamos el siguiente pseudocódigo:

    9 // P -> punto de interseccion
    10 // N -> normal a dicho punto
    11
    12 gpudriver->renderCone(cone_t(P,N,180),buffer);
    13 gpudriver->clear(background_color);
    14
    15 for each object in cone_t(P,N,180):
    16 {
    17 for each object face in cone_t(N,180):
    18 gpudriver->putTriangle(face.a, face.b, face.c,
    color_t(0));
    19 }
    20
    21 gpudriver->flush();
    22 color_t light=0;
    23 for each pixel in buffer:
    24 {
    25 contri=cos(angle(pixel,N));
    26 light+=contri*pixel;
    27 }
    28
    29 return light/buffer.numPixels;
    Como se ve, lo que hacemos es primer rellenar el buffer con el color del fondo. Acto seguido, lo que hacemos es pintar todos los triangulos de la escena de color negro. El resultado es un buffer en el que tenemos el cielo que puede verse desde ese punto. Si hacemos una medía de todos los pixels, tenemos una apróximación de la luz que llega desde el cielo.

    El problema de este método es que aproximadamente más de la mitad de las caras presentes en la escena tienen que volcarse para hacer el render. Cuando el número de caras en la escena es mucho mayor que el número de rayos necesarios para conseguir un resultado libre de ruido, es de esperar que la tarjeta gráfica lo haga más rápido que un método de raytracing. El problema esta en lo que pasa cuando el número de caras es muy elevado. En ese caso habría que experimentar que es más rapido:

    1. Lanzar 400 rayos contra 300000 caras.
    2. Hacer un render de 20x20 de 300000 caras.


    Es de esperar que 1 sea más rápido, sobre todo si dejamos crecer el número de triangulos de manera descontrolada. Pero habría que comprobar donde esta el límite. También cabría estudiar si se podría trabajar con versiones decimadas de la escena. En resumen, el render de escenas con esta iluminación en la GPU es algo a experimentar.

    GPU spherelight
    Cuando la fuente de luz deja de ser algo tan grande como un cielo, las expectativas en cuando a usar la GPU mejoran. Si nuestra fuente de luz es una esfera de pequeño tamaño como podría ser una lampara o una bombilla grande, el cono a renderizar disminuye.

    Ya no es necesario volcar la mitad de los triangulos de la escena. Sólo aquellos que estén dentro del cono que une el punto a sombrear con la esfera que actúa de fuente de luz. En un punto alejado de ésta, el número de triangulos implicados es de esperar que se aproxime al número de muestras que queremos tomar o incluso que sea inferior.

    Aquí es donde realmente nos podemos beneficiar del hardware gráfico. Un render de escasos polígonos sería mucho más rápido que lanzar 100 rayos contra la escena. En casos como estos podríamos permitirnos el lujo de tomar renders de 100x100, equivalente a 10000 muestras. Veamos un código de ejemplo:

    30 // P -> punto de interseccion
    31 // N -> normal a dicho punto
    32 // C -> centro de la esfera
    33 // R -> radio de la esfera
    34
    35 ángulo=atan(R/distance(C,P));
    36 direccion=C-P;
    37 gpudriver->renderCone(cone_t(P,direccion,ángulo),
    buffer);
    38 gpudriver->clear(color_t(0));
    39 for each face in esfera:
    40 gpudriver->putTriangle(face.a, face.b, face.c,
    color_esfera);
    41
    42 for each object in cone_t(P,direccion,ángulo):
    43 {
    44 for each object face in cone_t(P,direccion,ángulo):
    45 gpudriver->putTriangle(face.a, face.b, face.c,
    color_t(0));
    46 }
    47
    48 gpudriver->flush();
    49 color_t light=0;
    50 for each pixel in buffer:
    51 {
    52 contri=cos(angle(pixel,N));
    53 light+=contri*pixel;
    54 }
    55
    56 return 4*light/(buffer.numPixels*M_PI);
    El render se hace siempre sobre buffers cuadrados, dado que el elemento emisor renderizado sobre él es un círculo, quedan puntos en negro que debemos ignorar. El factor 4/π del cómputo final se aplica para compensar esos puntos que en principio estamos contando como sombra cuando no lo son. Otra forma más eficiente sería esquivar de la suma esos puntos. Se ha puesto así por hacer el código más sencillo.

    Este mismo método podría usarse para calcular reflexiones borrosas. Esto es comúnmente llamado “conetracing”, y sigue el mismo es quema el método de la esfera. La diferencia es que para tal cosa necesitaríamos una apróximación de la escena que incluya la iluminación, no servirían caras negras sin más.

    GPU ambient occlusion
    Éste es otro método que podría beneficiarse ampliamente del hardware gráfico. El ambient occlusion es un sistema muy parecido a la hemilight. La diferencia es que sólo se tienen en cuenta los triangulos que están a una distancia menor a un radio predefinido.

    Es una forma de estimar la cantidad de luz ambiente que llega a un punto de la escena. Dado que ignora todo lo que esté más allá de cierto radio. Por este motivo es aún más falos de lo que produce la hemilight. No obstante, este método goza de gran popularidad entre los usuarios de Blender. Dado que el número de triangulos a volcar en el render esta limitado por el radio predefinido, no sobrecargamos el hardware tanto como hacemos con la hemilight.



    El código apróximado sería parecido al siguiente:

    57 // P -> punto de interseccion
    58 // N -> normal a dicho punto
    59 // R -> radio de occlusion
    60
    61 gpudriver->renderCone(cone_t(P,N,180),buffer);
    62
    63 for each object in esfera(P,R):
    64 {
    65 for each object face in esfera(P,R):
    66 gpudriver->putTriangle(face.a, face.b, face.c,
    color_t(0));
    67 }
    68
    69 gpudriver->flush();
    70 float occlusion
    71 for each pixel in buffer:
    72 {
    73 occ=depth(pixel)/R;
    74 if(occ>1) occlusion+=1; else occlusion+=occ;
    75 }
    76
    77 return ambient_color*occlusion/buffer.numPixels
    Se calcula la oclusión medía en base a las distancias a los objetos que están en ese radio. El resultado se multiplica por el color de la luz ambiente y se devuelve. Nótese que asumimos que el driver gráfico nos puede proporcionar la profundidad de cada pixel renderizado.

    Conclusión
    Con este diseño del interfaz de las fuentes de la luz podemos implementar el calculo de la energía en un punto de manera independiente. Esto es lo que permite plantearse el usar el hardware gráfico para estimar estas energías y ahorrar cómputo. Es posible que todo esto sea un esfuerzo en vano. Este documento no pretende ser más que un compendio de ideas a tener en cuenta para el futuro del desarrollo de yafray.

    Última edición por 3dpoder; 10-02-2009 a las 02:16

  4. #19
    Administrador y fundador. Avatar de 3dpoder
    Fecha de ingreso
    Apr 2002
    Mensajes
    15,460

    Blender Modelado 3D basado en Bocetos

    Modelado 3D basado en Bocetos
    Inmaculada Gijón Cardós
    Carlos Gonzalez Morcillo
    inmagijon@gmail.com · Carlos.Gonzalez@uclm.es ::


    La fase de modelado de en proyectos 3D es difícil y tediosa en muchos casos, más aún cuando son necesarias superficies organicas. En la mayoría de las ocasiones, se parte de una primitiva y, aplicando transformaciónes geométricas, se obtiene el objeto deseado.

    Este proyecto presenta un prototipo de sistema que utiliza los trazos dibujados por el usuario para construir directamente una superficie tridimensional implícita. Nuestra apróximación difiere de las propuestas en trabajos previos (como Teddy de Takeo Igarashi) en que es un enfoque general y que implementa, con la misma apróximacion, la creación de nuevos objetos y la operación de extrusión (positiva y negativa).

    Introducción
    La forma general de construir objetos mediante modelado sólido es comenzar mediante una primitiva y, empleando operadores y transformaciónes, construir un modelo modificado. En una primera clasificación, podemos hablar de modelado poligonal y modelado basado en superficies curvas.

    Desde principios de los 90 se han desarrollado técnicas que permiten la construcción de modelos orgánicos de una forma rápida, como las superficies de subdivisión que comienzan con una Malla de Control que es subdividida recursivamente para obtener un resultado suave denominado Superficie Límite.

    Otra apróximación es el uso de superficies implícitas (con trabajos previos en la construcción de una superficie que cubre un esqueleto). Nuestra apróximación se basa en la silueta dibujada por el usuario. En la figura 1 se muestra el interfaz de usuario del prototipo denominado MoSkIS (Modelling from Sketches Implicit Surfaces). En esta figura se calcula la espina dorsal del modelo que se emplea para situar las meta-elipses que formaran nuestro modelo.


    Figura 1. Interfaz de usuario del prototipo MoSkIS 3D.

    Creación de un objeto
    A continuación se hace una descripción del algoritmo que MoSkIS 3D desarrolla para llevar a cabo la generación de un objeto 3D a partir de un trazo libre realizado por el usuario.

    Construcción de la silueta
    El usuario dibuja un contorno mediante un trazo libre, el cual debe ser cerrado y no producir intersecciones consigo mismo, ya que en este caso el algoritmo fallaría.

    A partir de este trazo se genera un polígono plano cerrado mediante la conexión del punto de comienzo y del punto final del trazo. Se identifican las aristas de este polígono mediante el ángulo mínimo que es requerido entre aristas consecutivas para crear una nueva arista.



    Figura 2. Izqda: Vértices convexos a triangularizar. Drcha: División del polígono en dos.

    Este parámetro es decidido por el usuario, al cual se le permite la opción de seleccionar el nivel de detalle con el que se construira dicho polígono. A más detalle, más vértices y más coste computacional pero más exactitud en el objeto final obtenido.
    El polígono que es generado a partir de este contorno dibujado, servirá como silueta del objeto 3D obtenido.

    Obtención de la superficie
    A la hora de obtener una superficie (compuesta de triangulos) a partir del polígono generado (ver figura 3 drcha) se realizan los siguientes pasos:

    1. Triangularización básica: se realiza identificando sucesivamente los vértices convexos del polígono, concretamente cuatro, que son sobre los que se tiene la seguridad de ser convesos, ya que la figura puede ser tan complicada, que de lugar a errores fácilmente en esta identificación. Los vértices convexos de un polígono con toda seguridad, son los situados en los extremos, tanto del eje X, como del eje Y, es decir, el vértice superior, el vértice inferior, el vértice situado más a la derecha y el vértice situado más a la izquierda. Para cada uno de estos vértices se evalúa si es posible el trazado de la arista interna que va a formar un triángulo con las dos aristas que comparten el vértice detectado, y si es posible la construcción de dicho triangulo, ese vértice es eliminado del proceso de triangularización junto con las dos aristas que comparte, al estar ya triangularizada esa zona, con lo cual, el problema se va reduciendo.

    Para que se lleve a cabo la construcción del triangulo, la arista interna a construir no puede cortar a ninguna otra arista del polígono (ver figura 2 Izqda.), es decir, no puede existir ningún vértice del polígono dentro del área del triángulo a construir ya que en ese caso la la triangularización sería errónea.



    Figura 3. Izqda: Flip de arista ilegal. Drcha: Resultado de Delaunay con la catalogación de los tres tipos de triangulos.

    A la hora de construir un triangulo, se tiene que asegurar que la arista no corta a ninguna otra arista del polígono (ver figura 2 Izqda.), es decir, que no hay parte del polígono dentro de la superficie del triángulo a construir, ya que en ese caso, se alteraría la figura al no generar los triangulos en la superficie que encierra el polígono.

    Como se puede ver en la figura 2 Izqda., no sería valida la arista interna que se ve punteada en el vértice superior. Para solucionar esta situación, se traza una arista interna que va desde este vértice hasta el más próximo a él de los que permanecen en interior del triángulo que se pretendía formar. Esta arista divide el polígono en dos (ver figura 2 Drcha.) y de esta forma se sigue aplicando una triangularización básica a cada uno de los polígonos (se divide el problema).

    En la figura 2 Drcha. se observa también que el vértice inferior detectado en el polígono P2, aunque sea convexo y no contenga parte del polígono en el área que del triángulo a trazar, choca con otra arista interna de la triangularización, este caso nunca se da porque si ya se ha trazado la arista interna para formar un triángulo con las aristas que comparten el vértice situado más a la izquierda, este vértice y aristas se han eliminado del proceso de triangularización, con lo cual, la arista valida es la que vemos en color rojo.

    De esta forma queda triangularizada toda la superficie limitada por el polígono cerrado generado.

    Se denominan aristas externas, a las aristas que definen el polígono o silueta generada y aristas internas, a las aristas que han sido generadas mediante triangularización (ver figura 3 Drcha).

    2. Delaunay: a la triangularización básica generada, se le aplica la optimización de Delaunay, consistente en la identificación y sustitución de aristas internas ilegales por aristas internas legales.



    Figura 4. Espina Dorsal.

    Una arista es legal cuando maximiza el ángulo mínimo del triángulo formado, y es ilegal en caso contrario. Si se identifica una arista ilegal, se realiza un giro (también denominado flip) de esa arista, por la opuesta en el cuadrilatero formado por los dos triangulos que comparten dicha arista.

    De esta forma se asegura que la superficie del polígono quede repartida mediante triangulos de la forma más homogénea posible (ver figura 3 Izqda.).

    Los triangulos generados a partir de la triangularización se pueden catalogar como (ver figura 3 Drcha.):

    • T-triangles: compuestos de dos aristas externas.
    • S-triangles: compuestos de una arista externa.
    • J-triangles: compuestos de tres aristas internas.


    Detección y elevación de la espina dorsal
    Una vez generado el plano, se procede a detectar la espina dorsal (spine) de éste. Para ello se detectan los puntos principales del eje mediano de la forma inicial, a lo que llamaremos puntos de conexión de la espina dorsal. La unión mediante aristas de estos puntos de conexión me dan la espina dorsal del plano que representa la silueta del objeto.

    Estos puntos de conexión se calculan recorriendo cada uno de los triangulos que forman el plano y analizando cada uno de ellos como se explica a continuación (ver figura 4):

    T-triangle: se detecta un punto de conexión en el centro de la arista interna del triangulo. Este punto también será detectado al evaluar el otro triángulo que comparte esta misma arista interna, y si es un S-triangle o un J-Triangle unira este punto de conexión con el que corresponda según se explica a continuación.


    • S-triangles: se detectan dos puntos de conexión, uno por cada una de las aristas internas del triángulo y serán situados en el centro de cada arista. Estos dos puntos de conexión serán unidos por una arista para formar la espina dorsal.


    • J-triangles: se detectan cuatro puntos de conexión, uno por cada una de las aristas internas, situados cada uno en el punto medio de las correspondientes aristas, y un punto de conexión situado en el centro del triangulo. Cada punto de conexión pertenecientes a las aristas internas serán unidos mediante una arista de la espina dorsal al punto de conexión del centro del triangulo.


    Una vez que todos los puntos de conexión han sido unidos mediante aristas formando la espina dorsal del plano, se procede a la elevación de la espina (ver figura 5).
    El problema es computar la distancia desde la espina dorsal al límite del polígono. Lo ideal es construir secciones representativas exactas de la forma, en cada uno de los vértices de la espina dorsal, cuyo coste es bastante elevado. Por ello se calcula, para cada punto de conexión, dependiendo de si esta asociado a una arista interna o esta situado en el centro de un triangulo.

    • Para el caso en el que los puntos de conexión sean punto medio de aristas internas, se calcula un promedio de las distancias entre los puntos de conexión y los vértices externos (cualquier vértice del plano) que están conectados directamente a ese vértice, y esa será la altura de ese punto de la espina.


    • Para el caso en el que los puntos de conexión son el punto central de un J-triangle, se calcula como promedio de las distancias de elevación de los puntos de conexión directamente conectados a éste.


    Figura 5. Elevación de la espina dorsal.


    Figura 6. Izqda: Dimensiones X e Y y orientación de cada metaelipse. Drcha: Dimension Z de cada metaelipse.

    Obtención del volumen mediante metasuperficies
    La obtención del volumen se realiza añadiendo para cada vértice o punto de conexión de la espina dorsal un metaelemento, concrétamente una metaelipse con una dimensión y orientación determinada (ver figura 6).

    • La dimensión X de cada metaelipse se corresponde para el caso en el que su punto de conexión asociado es el punto medio de una arista interna, con la longitud de dicha arista interna; si por el contrario, el punto de conexión asociado se corresponde con el punto medio de un J-triangle, la dimensión X se corresponde con la longitud medía de las bisectrices del J-triangle.


    • La dimensión Y de cada metaelipse se corresponde con la distancia medía desde ese punto de conexión al resto de puntos de conexión directamente conectados al mismo.


    • La dimensión Z de cada metaelipse se corresponde con el doble de la altura de la espina en el punto de conexión asociado a la metaelipse, ya que la figura resultante va a ser simétrica, y tendrá una componente Z positiva de valor el de la altura de la espina y una componente Z negativa del mismo valor absoluto (ver figura 6 Drcha.).


    Con este algoritmo se obtiene una malla 3D creada a partir de la unión de esas metaelipses añadidas, consiguiendo el objeto 3D con las dimensiones correspondientes a la forma inicial pintada por el usuario.


    Figura 7. Izqda: Metaelipses de dimensión fija y reducida. Drcha: Metaelipses a lo largo del eje Z.

    Mejoras en la obtención del volumen
    En la generación del objeto 3D según el algoritmo de generación de volumen especificado en el apartado anterior surgen dos problemas principales:

    • Cuando en la generación de la espina dorsal, aparecen por ejemplo dos puntos de conexión muy distantes, pero que a sus otros lados respectivamente tienen puntos de conexión muy cercanos, como la dimensión Y que se establece para cada metabola es la medía de las distancias, pueden aparecer huecos o cavidades en la malla 3D del objeto resultante debido a la falta de metaelementos en esa zona.


    • En algunas figuras creadas por el usuario, se identifican saltos bruscos entre las metasuperficies, cuando en realidad, no se debía poder identificar visualmente cada metasuperficie, sino que se debería obtener una malla suavizada como resultado del trazo realizado originariamente por el usuario. Estos saltos son más destacados a cuanto menor es el nivel de detalle. A mayor nivel de detalle en la generación del polígono a partir del trazo del usuario, más vértices obtenemos, por lo tanto más triangulos son generados en la triangularización y más puntos de conexión obtendremos en la construcción de la espina dorsal, con lo cual las dimensiones de los metaelementos cambian de uno a otro más progresivamente.

    La solución a estos problemas pasa por la creación de metaelipses de dimensiones fijas reducidas, en vez de una por cada punto de conexión de la espina dorsal, crear las necesarias para cubrir todas las dimensiones (ver figura 7 Izqda.).

    • Se sitúan metaelipses con dimensiones fijas y reducidas a lo largo de cada arista interna.


    • Se sitúan metaelipses a lo largo de la mayor bisectriz de cada Jtriangle.


    • Se sitúan metaelipses a lo largo del eje de la espina dorsal, y cada cierta distancia, en ausencia de aristas internas, se crearan aristas internas imaginarias, con el mismo comportamiento que el citado anteriormente para la situación de metaelipses. Con esta decisión, se evita la generación de huecos indeseados en el objeto 3D generado.


    • Se sitúan metaelipses cubriendo la distancia medía desde el punto de conexión con los puntos conectados directamente.


    • Se sitúan metaelipses a lo largo del eje Z cubriendo la altura de la espina dorsal, estableciendo una altura incremental desde la altura de un punto de conexión al siguiente. Así evitamos escalones bruscos en la malla 3D generada.

    Como se puede ver en la figura 7 Drcha., (la figura ha sido simplificada viéndose únicamente metaelipses a lo largo de la espina dorsal para mayor claridad), se muestra hasta dónde llegarían las metaelipses a lo largo del eje Z, estableciendo como se ha dicho anteriormente un promedio entre la posición (en el eje Z) de las que están situadas en la posición de los puntos de conexión que las limitan.

    Algoritmo de extrusión
    A continuación se describe el algoritmo para la ejecución de la operación de extrusión sobre un objeto 3D previamente creado. Este algoritmo aún se encuentra en desarrollo en el sistema MOSKIS3D.

    Trazo del usuario
    En primer lugar, el usuario realiza un trazo seleccionando la superficie del objeto 3D a extruir, ese trazo, también llamado trazo base, debe formar una superficie cerrada. A continuación, realiza otro trazo, llamado trazo de extrusión, con la forma que desea conseguir con la operación (ver figura 8 Izqda.).

    El algoritmo de extrusión, crea una nueva malla 3D basada en el trazo base creado por el usuario y en el trazo de extrusión.


    Figura 8. Izqda: Trazos del usuario al realizar la operación de extrusión. Drcha: Polígono triangularizado.

    El usuario puede desear una extrusión positiva, es decir, una prolongación del objeto 3D hacia el exterior siguiendo la forma del trazo realizado; o una extrusión negativa, es decir, una concavidad en el interior del objeto 3D con la forma indicada (ver figura 8 Izqda.).

    A partir del trazo base, se construye un polígono plano cerrado con el mismo nivel de detalle que el establecido para la generación del objeto 3D previo.

    A partir del trazo de extrusión, también se genera un polígono plano, pero abierto, que tendrá el mismo nivel de detalle que en el caso anterior (ver figura 8 Drcha.).

    Triangularización y generación de la espina
    Una vez obtenido el polígono del trazo base, se procede a la triangularización de ese polígono cerrado, al igual que se hacía en el caso de creación de un objeto nuevo. Primero se triangulariza mediante un algoritmo de triangularización básica y a continuación se procede a una optimización por Delaunay (ver figura 3 Drcha).

    Una vez triangularizado el trazo base, se tiene una superficie compuesta de los tres tipos de triangulos citados anteriormente T-triangles, J-triangles y S-triangles (ver figura 3 Drcha).

    En este momento, se calcula la espina dorsal del plano generado a partir del trazo base, evaluando cada uno de los triangulos como se hacía en la detección de la espina para el caso de creación de un objeto nuevo (ver figura 4), obteniendo una serie de puntos conectados formando el eje central de la superficie seleccionada por el usuario, es decir, la superficie a extruir.



    Figura 9. Izqda: Trazo base triangularizado y espina dorsal detectada. Drcha: Anillos de extrusión.

    En la creación de un objeto nuevo, el siguiente paso era la elevación de la espina dorsal mediante el calculo de un promedio en cada uno de los puntos de conexión de la espina. En el algoritmo de extrusión, por el contrario, no se quiere crear un objeto 3D a partir de la forma creada por el usuario como trazo base, sino extruir esa forma a lo largo del trazo de extrusión, por lo que no tiene sentido el calculo del objeto 3D asociado a esa forma, y por tanto, no tiene sentido el paso de elevación de la espina dorsal (ver figura 9 Izqda.).

    Generación de la extrusión
    Una vez obtenido el plano correspondiente al trazo base, y la espina dorsal de dicho plano, se procede a rellenar el plano con metaelipses.

    Se sitúan metaelipses con dimensiones fijas y reducidas a lo largo de cada arista interna, para cada triangulo, y para el caso en el que se tenga un Jtriangle se sitúan metaelipses a lo largo de la mayor bisectriz del triangulo.

    Para evitar la generación de huecos indeseados, se sitúan metaelipses a lo largo del eje de la espina dorsal, y cada cierta distancia, si no existen aristas internas, se crearan imaginarias, con el mismo comportamiento. También se sitúan metaelipses cubriendo la distancia medía entre el punto de conexión y los puntos conectados directamente a este.

    Con este paso obtenemos un conjunto de metabolas con la forma de la superficie que delimitaba el trazo base generado originalmente por el usuario. Para generar la extrusión del trazo base a lo largo del trazo de extrusión se procede a la proyección del conjunto de metabolas, de forma que cada una de las proyecciones resultantes queden situadas perpendicularmente con respecto al trazo de extrusión y se redimensionen para ajustarse al tamaño del trazo.

    El número de proyecciones en principio depende del nivel de detalle con el cual se haya generado el polígono abierto correspondiente al trazo de extrusión creado por el usuario. En los casos en los que la distancia entre los vértices del polígono (del trazo de proyección) sea muy grande, para evitar el problema de zonas huecas o poco pobladas del que hablábamos en el apartado de Mejoras en la obtención del volumen, delimitaremos la distancia máxima que debe haber para una nueva proyección, y en el caso en el que no se cumpla, se añadira una nueva proyección (ver figura 9 Drcha).

    Hay que diferenciar el caso en el que la extrusión es positiva o negativa, ya que en el primer caso las metaelipses generadas deben tener una atracción positiva, mientras que en el caso en el que la extrusión es negativa, se producira una repulsión, para conseguir la concavidad o hueco que se desea (ver figura 8 Izqda).

    Resultados
    Empleando el método explicado en este artículo hemos obtenido el modelo de la figura 10. La silueta original fue convertida a 431 superficies implícitas (incluyendo las necesarias para realizar las extrusiones). El siguiente paso fue convertir la superficie a malla poligonal (formada por 12403 caras). Finalmente, el modelo fue suavizado y se aplicó un algoritmo de eliminación de aristas.


    Figura 10. Ejemplo de personaje realizado con MoSkIS 3D.

    Última edición por 3dpoder; 10-02-2009 a las 03:00

Página 2 de 2 PrimerPrimer 12

Temas similares

  1. Respuestas: 0
    Último mensaje: 22-02-2015, 10:37
  2. Nueva web IMaGEN DIGITaL 3D
    Por acortes en el foro Esta es mi página...
    Respuestas: 10
    Último mensaje: 15-10-2011, 03:43
  3. Blender Herramientas Libres para Sintesis 3D
    Por 3dpoder en el foro Un Enfoque práctico a Blender
    Respuestas: 0
    Último mensaje: 10-09-2009, 01:55
  4. Respuestas: 0
    Último mensaje: 17-01-2009, 14:00
  5. Blendiberia 2006
    Por morcy en el foro Noticias
    Respuestas: 251
    Último mensaje: 08-09-2006, 00:48

Actualmente estos son sus permisos de publicación en el foro.

  • -No puedes crear nuevos temas al no estar registrado o no haber iniciado sesión en el foro.
  • -No puedes responder temas al no estar registrado o no haber iniciado sesión en el foro.
  • -No puedes subir archivos adjuntos al no estar registrado o no haber iniciado sesión en el foro.
  • -No puedes editar tus mensajes al no estar registrado o no haber iniciado sesión en el foro.
  •