Ajout de python-obja

This commit is contained in:
Thomas Forgione 2021-07-22 15:19:40 +02:00
parent 61bf8f59ad
commit bd1ba14af7
11 changed files with 4482 additions and 4213 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
assets
__pycache__

View File

@ -12,6 +12,34 @@ Vous pouvez récupérer les sources de cette application en lançant la commande
git clone https://gitea.tforgione.fr/tforgione/obja
```
## Écriture d'un logiciel de compression progressive
Le module `obja.py` permet de parser facilement des fichiers OBJ et de générer
des fichiers au format OBJA.
La classe `obja.Model` permet de facilement parser un fichier OBJ (grâce à la
méthode `parse_file`. Elle contient les attributs suivants :
- `vertices` : une liste de `numpy.array` qui représente les sommets du
modèle (attention, les vecteurs sont en ligne)
- `faces` : une liste de `obja.Face`, qui contiennent eux-mêmes des attributs
`a`, `b` et `c` qui sont les indices des sommets dans l'attribut
`vertices` (les indices commencent à partir de 0).
La classe `obja.Output` permet de générer facilement un modèle OBJA. Lors de la
transformation d'un modèle pour l'adapter à un chargement progressif, le modèle
doit être reconstruit et les indices des sommets et faces sont changés. La
classe permet de travailler avec les indices du modèle d'origine, et donc de
gérer automatiquement la transformation des indices de l'ancien modèle vers le
nouveau modèle.
Le fichier `decimate.py` contient un exemple basique de programme permettant la
réécriture d'un fichier OBJ en OBJA de manière naïve. Il contient un programme
principal qui transforme le fichier `exemple/suzanne.obj` en
`exemple/suzanne.obja`, le rendant progressif.
## Visualisation du streaming
À la racine de ce projet, le script `server.py` vous permet de démarrer un
server de streaming. Vous pouvez l'exécuter en lançant `./server.py`. Une fois
cela fait, vous pouvez allez sur [localhost:8000](http://localhost:8000) pour
@ -20,14 +48,15 @@ les affiche.
Les modèles doivent être sauvegardés dans le dossiers `assets`, et peuvent être
visualisés en ajouter `?nom_du_modele.obj` à la fin de l'url. Par exemple,
[localhost:8000/?bunny.obj](http://localhost:8000/?bunny.obj) chargera le
modèle `bunny.obj` du dossier `assets`. Ce modèle est un modèle d'exemple, il
commence par encoder la version basse résolution du [Stanford
[localhost:8000/?exemple/suzanne.obja](http://localhost:8000/?exemple/suzanne.obja)
chargera le modèle `bunny.obj` du dossier `assets`. Ce modèle est un modèle
d'exemple, il commence par encoder la version basse résolution du [Stanford
bunny](https://graphics.stanford.edu/data/3Dscanrep/), translate tous ses
sommets, les retranslate vers leurs positions d'origine puis supprime toutes
les faces.
## Commandes
### Détails du format OBJA
###### Ajout d'un sommet
Comme dans le OBJ standard, pour ajouter un sommet, il suffit d'utiliser le

File diff suppressed because it is too large Load Diff

58
decimate.py Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env python
import obja
import numpy as np
import sys
class Decimater(obja.Model):
"""
A simple class that decimates a 3D model stupidly.
"""
def __init__(self):
super().__init__()
self.deleted_faces = set()
def contract(self, output):
"""
Decimates the model stupidly, and write the resulting obja in output.
"""
operations = []
# Iterate through the vertex
for (vertex_index, vertex) in enumerate(self.vertices):
for (face_index, face) in enumerate(self.faces):
# Delete any face that depends on this vertex
if face_index not in self.deleted_faces:
self.deleted_faces.add(face_index)
operations.append(('face', face_index, face))
# Delete the vertex
operations.append(('vertex', vertex_index, vertex))
# To rebuild the model, run operations in reverse order
operations.reverse()
output_model = obja.Output(output)
for (ty, index, value) in operations:
if ty == "vertex":
output_model.add_vertex(index, value)
else:
output_model.add_face(index, value)
def main():
"""
Runs the program on the model given as parameter.
"""
np.seterr(invalid = 'raise')
model = Decimater()
model.parse_file('exemple/suzanne.obj')
with open('exemple/suzanne.obja', 'w') as output:
model.contract(output)
if __name__ == '__main__':
main()

2580
exemple/suzanne.obj Normal file

File diff suppressed because it is too large Load Diff

1479
exemple/suzanne.obja Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ animate();
function init() {
let url = 'assets/' + (document.URL.split('?')[1] || "bunny.obj");
let url = (document.URL.split('?')[1] || "exemple/suzanne.obja");
loader = new Loader(url, 1024, 20);
loader.start(function(elements) {

View File

@ -29,8 +29,7 @@ function fetchData(path, start, end, callback) {
function parseLine(line, number) {
let element = {};
let split = line.split(/[ \t]+/);
let split = line.split('#')[0].split(/[ \t]+/);
if (split.length === 0) {
return;
}
@ -358,6 +357,7 @@ class Model extends THREE.Mesh {
normal.normalize();
f.normal = normal;
f.color = this.geometry.faces[element.id].color;
this.geometry.faces[element.id] = f;

326
obja.py Executable file
View File

@ -0,0 +1,326 @@
#!/usr/bin/env python3
import sys
import numpy as np
import random
"""
obja model for python.
"""
class Face:
"""
The class that holds a, b, and c, the indices of the vertices of the face.
"""
def __init__(self, a, b, c, visible = True):
self.a = a
self.b = b
self.c = c
self.visible = visible
def from_array(array):
"""
Initializes a face from an array of strings representing vector indices (starting at 1)
"""
face = Face(0, 0, 0)
face.set(array)
face.visible = True
return face
def set(self, array):
"""
Sets a face from an array of strings representing vector indices (starting at 1)
"""
self.a = int(array[0].split('/')[0]) - 1
self.b = int(array[1].split('/')[0]) - 1
self.c = int(array[2].split('/')[0]) - 1
return self
def clone(self):
"""
Clones a face from another face
"""
return Face(self.a, self.b, self.c, self.visible)
def copy(self, other):
"""
Sets a face from another face
"""
self.a = other.a
self.b = other.b
self.c = other.c
self.visible = other.visible
return self
def test(self, vertices, line = "unknown"):
"""
Tests if a face references only vertices that exist when the face is declared.
"""
if self.a >= len(vertices):
raise VertexError(self.a + 1, line)
if self.b >= len(vertices):
raise VertexError(self.b + 1, line)
if self.c >= len(vertices):
raise VertexError(self.c + 1, line)
def __str__(self):
return "Face({}, {}, {})".format(self.a, self.b, self.c)
def __repr__(self):
return str(self)
class VertexError(Exception):
"""
An operation references a vertex that does not exist.
"""
def __init__(self, index, line):
"""
Creates the error from index of the referenced vertex and the line where the error occured.
"""
self.line = line
self.index = index
super().__init__()
def __str__(self):
"""
Pretty prints the error.
"""
return f'There is no vector {self.index} (line {self.line})'
class FaceError(Exception):
"""
An operation references a face that does not exist.
"""
def __init__(self, index, line):
"""
Creates the error from index of the referenced face and the line where the error occured.
"""
self.line = line
self.index = index
super().__init__()
def __str__(self):
"""
Pretty prints the error.
"""
return f'There is no face {self.index} (line {self.line})'
class FaceVertexError(Exception):
"""
An operation references a face vector that does not exist.
"""
def __init__(self, index, line):
"""
Creates the error from index of the referenced face vector and the line where the error occured.
"""
self.line = line
self.index = index
super().__init__()
def __str__(self):
"""
Pretty prints the error.
"""
return f'Face has no vector {self.index} (line {self.line})'
class UnknownInstruction(Exception):
"""
An instruction is unknown.
"""
def __init__(self, instruction, line):
"""
Creates the error from instruction and the line where the error occured.
"""
self.line = line
self.instruction = instruction
super().__init__()
def __str__(self):
"""
Pretty prints the error.
"""
return f'Instruction {self.instruction} unknown (line {self.line})'
class Model:
"""
The OBJA model.
"""
def __init__(self):
"""
Intializes an empty model.
"""
self.vertices = []
self.faces = []
self.line = 0
def get_vector_from_string(self, string):
"""
Gets a vector from a string representing the index of the vector, starting at 1.
To get the vector from its index, simply use model.vertices[i].
"""
index = int(string) - 1
if index >= len(self.vertices):
raise FaceError(index + 1, self.line)
return self.vertices[index]
def get_face_from_string(self, string):
"""
Gets a face from a string representing the index of the face, starting at 1.
To get the face from its index, simply use model.faces[i].
"""
index = int(string) - 1
if index >= len(self.faces):
raise FaceError(index + 1, self.line)
return self.faces[index]
def parse_file(self, path):
"""
Parses an OBJA file.
"""
with open(path, "r") as file:
for line in file.readlines():
self.parse_line(line)
def parse_line(self, line):
"""
Parses a line of obja file.
"""
self.line += 1
split = line.split()
if len(split) == 0:
return
if split[0] == "v":
self.vertices.append(np.array(split[1:], np.double))
elif split[0] == "ev":
self.get_vector_from_string(split[1]).set(split[2:])
elif split[0] == "tv":
self.get_vector_from_string(split[1]).translate(split[2:])
elif split[0] == "f" or split[0] == "tf":
for i in range(1, len(split) - 2):
face = Face.from_array(split[i:i+3])
face.test(self.vertices, self.line)
self.faces.append(face)
elif split[0] == "ts":
for i in range(1, len(split) - 2):
if i % 2 == 1:
face = Face.from_array([split[i], split[i + 1], split[i + 2]])
else:
face = Face.from_array([split[i], split[i + 2], split[i + 1]])
face.test(self.vertices, self.line)
self.faces.append(face)
elif split[0] == "ef":
self.get_face_from_string(split[1]).set(split[2:])
elif split[0] == "efv":
face = self.get_face_from_string(split[1])
vector = int(split[2])
new_index = int(split[3]) - 1
if vector == 1:
face.a = new_index
elif vector == 2:
face.b = new_index
elif vector == 3:
face.c = new_index
else:
raise FaceVertexError(vector, self.line)
elif split[0] == "df":
self.get_face_from_string(split[1]).visible = False
elif split[0] == "#":
return
else:
return
# raise UnknownInstruction(split[0], self.line)
def parse_file(path):
"""
Parses a file and returns the model.
"""
model = Model()
model.parse_file(path)
return model
class Output:
"""
The type for a model that outputs as obja.
"""
def __init__(self, output, random_color = False):
"""
Initializes the index mapping dictionnaries.
"""
self.vertex_mapping = dict()
self.face_mapping = dict()
self.output = output
self.random_color = random_color
def add_vertex(self, index, vertex):
"""
Adds a new vertex to the model with the specified index.
"""
self.vertex_mapping[index] = len(self.vertex_mapping)
print('v {} {} {}'.format(vertex[0], vertex[1], vertex[2]), file = self.output)
def edit_vertex(self, index, vertex):
"""
Changes the coordinates of a vertex.
"""
print('ev {} {} {} {}'.format(self.vertex_mapping[index] + 1, vertex[0], vertex[1],vertex[2]), file = self.output)
def add_face(self, index, face):
"""
Adds a face to the model.
"""
self.face_mapping[index] = len(self.face_mapping)
print('f {} {} {}'.format(
self.vertex_mapping[face.a] + 1,
self.vertex_mapping[face.b] + 1,
self.vertex_mapping[face.c] + 1,
),
file = self.output
)
if self.random_color:
print('fc {} {} {} {}'.format(
len(self.face_mapping),
random.uniform(0, 1),
random.uniform(0, 1),
random.uniform(0, 1)),
file = self.output
)
def edit_face(self, index, face):
"""
Changes the indices of the vertices of the specified face.
"""
print('ef {} {} {} {}'.format(
self.face_mapping[index] + 1,
self.vertex_mapping[face.a] + 1,
self.vertex_mapping[face.b] + 1,
self.vertex_mapping[face.c] + 1
),
file = self.output
)
def main():
if len(sys.argv) == 1:
print("obja needs a path to an obja file")
return
model = parse_file(sys.argv[1])
print(model.vertices)
print(model.faces)
if __name__ == "__main__":
main()

View File

@ -29,8 +29,7 @@ function fetchData(path, start, end, callback) {
function parseLine(line, number) {
let element = {};
let split = line.split(/[ \t]+/);
let split = line.split('#')[0].split(/[ \t]+/);
if (split.length === 0) {
return;
}

View File

@ -137,6 +137,7 @@ class Model extends THREE.Mesh {
normal.normalize();
f.normal = normal;
f.color = this.geometry.faces[element.id].color;
this.geometry.faces[element.id] = f;