Update, docs

This commit is contained in:
Thomas Forgione 2019-12-11 17:49:13 +01:00
parent 3328393f1d
commit 4b8760c3e3
No known key found for this signature in database
GPG Key ID: BFD17A2D71B3B5E7
6 changed files with 443 additions and 4 deletions

1
.gitignore vendored
View File

@ -1,2 +1 @@
assets
js/main.js

View File

@ -1,10 +1,9 @@
all: js
OUTPUT=js/main.js
OUTPUT=js/obja.js
js: src/*
rm -f ${OUTPUT}
cat src/Loader.js >> ${OUTPUT} && echo >> ${OUTPUT}
cat src/Model.js >> ${OUTPUT} && echo >> ${OUTPUT}
cat src/main.js >> ${OUTPUT} && echo >> ${OUTPUT}

127
README.md Normal file
View File

@ -0,0 +1,127 @@
# OBJ augmenté
WaveFront OBJ est un format permettant d'encoder des modèles 3D de manière
simple. Cependant, il n'est pas adapté aux représentations progressives. Pour
cela, nous avons augmenté OBJ de nouvelles commandes qui permettent de modifier
le contenu préalablement déclaré.
## Commandes
###### Ajout d'un sommet
Comme dans le OBJ standard, pour ajouter un sommet, il suffit d'utiliser le
caractère `v` suivi des coordonnées du sommet. Par exemple :
```
v 1.0 2.0 3.0
```
###### Ajout d'une face
Comme dans le OBJ standard, pour ajouter une face, il suffit d'utiliser le
caractère `f` suivi des indices des sommets de la face. Par exemple :
```
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 1.0 1.0 0.0
f 1 2 3
```
**Attention :** en OBJ, les indices commencent à partir de 1
**Attention :** dans notre logiciel, seule les faces triangulaires sont supportées.
###### Edition d'un sommet
Notre format OBJ permet la modification d'un ancien sommet. Pour modifier un
sommet, il suffit d'utiliser les caractères `ev` suivis de l'indice du sommet à
modifier puis de ses nouvelles coordonées. Par exemple :
```
v 0.0 0.0 0.0
ev 1 1.0 1.0 1.0
```
###### Edition d'une face
Notre format OBJ permet la modification d'une ancienne face. Pour modifier une
face, il suffit d'utiliser les caractères `ef` suivis de l'indice de la face à
modifier puis des indices de ses nouveaux sommets. Par exemple :
```
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 1.0 1.0 0.0
v 1.0 1.0 1.0
f 1 2 3
ef 1 1 2 4
```
###### Suppression d'une face
Notre format OBJ permet la suppression d'une ancienne face. Pour supprimer une
face, il suffit d'utiliser les caracètres `df` suivis de l'indice de la face à
supprimer. Par exemple :
```
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 1.0 1.0 0.0
v 1.0 1.0 1.0
f 1 2 3
df 1
```
**Attention :** les indices des faces ne sont pas changés après la suppression
d'une ancienne face.
###### Triangle strips et triangle fans
Pour la compression de contenu 3D, on utilise souvent des [Triangle
Strips](https://en.wikipedia.org/wiki/Triangle_strip) et des [Triangle
Fans](https://en.wikipedia.org/wiki/Triangle_fan).
Notre format OBJ augmenté permet la déclaration de strips et de fans en
utilisant respectivement les caractères `ts` et `tf` suivis des indices des
sommets. Par exemple :
```
v -1.0 0.0 0.0
v -0.5 1.0 0.0
v 0.0 0.0 0.0
v 0.5 1.0 0.0
v 1.0 0.0 0.0
ts 1 2 3 4 5
```
ou bien
```
v 0.0 0.0 0.0
v -1.0 0.0 0.0
v -0.707 0.707 0.0
v 0.0 1.0 0.0
v 0.707 0.707 0.0
v 1.0 0.0 0.0
tf 1 2 3 4 5 6
```
## Utilisation
Vous pouvez récupérer les sources de cette application en lançant la commande
```
git clone https://gitea.tforgione.fr/tforgione/obja
```
À 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
lancer le streaming. Le navigateur télécharge progressivement les données et
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
bunny](https://graphics.stanford.edu/data/3Dscanrep/), translate tous ses
sommets, les retranslate vers leurs positions d'origines puis supprime toutes
les faces.

View File

@ -37,6 +37,7 @@
</div>
<script src="js/three.min.js"></script>
<script src="js/OrbitControls.js"></script>
<script src="js/obja.js"></script>
<script src="js/main.js"></script>
</body>
</html>

View File

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

313
js/obja.js Normal file
View File

@ -0,0 +1,313 @@
function fetchDataLength(path, callback) {
let xhr = new XMLHttpRequest();
xhr.open('HEAD', path, true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
callback(xhr.getResponseHeader('Content-Length'));
}
}
};
xhr.send();
}
function fetchData(path, start, end, callback) {
let xhr = new XMLHttpRequest();
xhr.open('GET', path, true);
xhr.setRequestHeader('Range', 'bytes=' + start + "-" + (end - 1));
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 206) {
callback(xhr.responseText);
}
}
};
xhr.send();
}
function parseLine(line) {
let element = {};
let split = line.split(/[ \t]+/);
if (split.length === 0) {
return;
}
switch (split[0]) {
case "v":
element.type = Element.AddVertex;
element.value = new THREE.Vector3(
parseFloat(split[1]),
parseFloat(split[2]),
parseFloat(split[3]),
);
return element;
case "f":
element.type = Element.AddFace;
element.value = new THREE.Face3(
parseInt(split[1], 10) - 1,
parseInt(split[2], 10) - 1,
parseInt(split[3], 10) - 1,
);
return element;
case "ts":
element.type = Element.AddTriangleStrip;
element.value = [];
for (let i = 1; i < split.length - 2; i++) {
element.value.push(new THREE.Face3(
parseInt(split[i] , 10) - 1,
i % 2 === 1 ? parseInt(split[i+1], 10) - 1 : parseInt(split[i+2], 10) - 1,
i % 2 === 1 ? parseInt(split[i+2], 10) - 1 : parseInt(split[i+1], 10) - 1,
));
}
return element;
case "tf":
element.type = Element.AddTriangleFan;
element.value = [];
for (let i = 1; i < split.length - 2; i++) {
element.value.push(new THREE.Face3(
parseInt(split[1] , 10) - 1,
parseInt(split[i+1], 10) - 1,
parseInt(split[i+2], 10) - 1,
));
}
return element;
case "ev":
element.type = Element.EditVertex;
element.id = parseInt(split[1], 10) - 1;
element.value = new THREE.Vector3(
parseFloat(split[2]),
parseFloat(split[3]),
parseFloat(split[4]),
);
return element;
case "ef":
element.type = Element.EditFace;
element.id = parseInt(split[1], 10) - 1;
element.value = new THREE.Face3(
parseInt(split[2], 10) - 1,
parseInt(split[3], 10) - 1,
parseInt(split[4], 10) - 1,
);
return element;
case "df":
element.type = Element.DeleteFace;
element.id = parseInt(split[1], 10) - 1;
return element;
case "":
case "#":
return;
default:
throw new Error(split[0] + " is not a defined macro");
}
}
const Element = {};
Element.AddVertex = "AddVertex";
Element.AddFace = "AddFace";
Element.AddTriangleStrip = "AddTriangleStrip";
Element.AddTriangleFan = "AddTriangleFan";
Element.EditVertex = "EditVertex";
Element.EditFace = "EditFace";
Element.DeleteFace = "DeleteFace";
class Loader {
constructor(path, chunkSize = 1024, timeout = 20) {
this.path = path;
this.chunkSize = chunkSize;
this.timeout = timeout;
this.currentByte = 0;
this.remainder = "";
}
start(callback) {
fetchDataLength(this.path, (length) => {
this.dataLength = length;
this.next(callback);
});
}
percentage() {
return 100 * this.currentByte / this.dataLength;
}
next(callback) {
this.downloadAndParseNextChunk((data) => {
callback(data);
setTimeout(() => {
this.next(callback);
}, this.timeout);
});
}
downloadAndParseNextChunk(callback) {
let upperBound = Math.min(this.currentByte + this.chunkSize, this.dataLength);
if (upperBound <= this.currentByte) {
return;
}
fetchData(this.path, this.currentByte, upperBound, (data) => {
this.currentByte = upperBound;
let elements = [];
let split = data.split('\n');
split[0] = this.remainder + split[0];
this.remainder = split.pop();
for (let line of split) {
elements.push(parseLine(line));
}
callback(elements);
});
}
}
class Model extends THREE.Mesh {
constructor(path) {
let geometry = new THREE.Geometry();
let materials = [
new THREE.MeshLambertMaterial( { color: 0xffffff, side: THREE.DoubleSide } ),
new THREE.MeshBasicMaterial( { transparent: true, opacity: 0 } )
];
super(geometry, materials);
this.frustumCulled = false;
this.path = path;
this.vertices = [];
this.currentLine = 1;
}
throwError(message) {
let e = new Error("In " + this.path + ":L" + this.currentLine + " " + message);
e.type = "custom";
throw e;
}
checkVertex(id) {
if (this.geometry.vertices[id] === undefined) {
this.throwError("EditVertex requires vertex " + (id + 1) + " but there is no such vertex");
}
}
checkFaceId(id) {
if (this.geometry.faces[id] === undefined) {
this.throwError("EditFace requires face " + (id + 1) + " but there is no such face");
}
}
checkFace(f) {
let vertices = this.geometry.vertices;
if (vertices[f.a] === undefined) {
this.throwError("Face requires vertex " + (f.a + 1) + " but there is no such vertex");
}
if (vertices[f.b] === undefined) {
this.throwError("Face requires vertex " + (f.b + 1) + " but there is no such vertex");
}
if (vertices[f.c] === undefined) {
this.throwError("Face requires vertex " + (f.c + 1) + " but there is no such vertex");
}
}
manageElement(element) {
let vertices = this.geometry.vertices;
let f, normal;
switch (element.type) {
case Element.AddVertex:
this.geometry.vertices.push(element.value);
this.geometry.verticesNeedUpdate = true;
break;
case Element.EditVertex:
this.checkVertex(element.id);
this.geometry.vertices[element.id].copy(element.value);
this.geometry.verticesNeedUpdate = true;
break;
case Element.AddFace:
f = element.value;
this.checkFace(f);
normal =
vertices[f.b].clone().sub(vertices[f.a])
.cross(vertices[f.c].clone().sub(vertices[f.a]));
normal.normalize();
f.normal = normal;
f.materialIndex = 0;
this.geometry.faces.push(f);
this.geometry.elementsNeedUpdate = true;
break;
case Element.AddTriangleStrip:
case Element.AddTriangleFan:
for (let f of element.value) {
this.checkFace(f);
let normal =
vertices[f.b].clone().sub(vertices[f.a])
.cross(vertices[f.c].clone().sub(vertices[f.a]));
normal.normalize();
f.normal = normal;
f.materialIndex = 0;
this.geometry.faces.push(f);
}
this.geometry.elementsNeedUpdate = true;
break;
case Element.EditFace:
f = element.value;
this.checkFaceId(element.id);
this.checkFace(f);
normal =
vertices[f.b].clone().sub(vertices[f.a])
.cross(vertices[f.c].clone().sub(vertices[f.a]));
normal.normalize();
f.normal = normal;
this.geometry.faces[element.id] = f;
this.geometry.elementsNeedUpdate = true;
break;
case Element.DeleteFace:
this.geometry.faces[element.id].materialIndex = 1;
this.geometry.elementsNeedUpdate = true;
break;
default:
throw new Error("unknown element type: " + element.type);
}
this.currentLine++;
}
}