Update, docs
This commit is contained in:
parent
3328393f1d
commit
4b8760c3e3
|
@ -1,2 +1 @@
|
||||||
assets
|
assets
|
||||||
js/main.js
|
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -1,10 +1,9 @@
|
||||||
all: js
|
all: js
|
||||||
|
|
||||||
OUTPUT=js/main.js
|
OUTPUT=js/obja.js
|
||||||
|
|
||||||
js: src/*
|
js: src/*
|
||||||
rm -f ${OUTPUT}
|
rm -f ${OUTPUT}
|
||||||
cat src/Loader.js >> ${OUTPUT} && echo >> ${OUTPUT}
|
cat src/Loader.js >> ${OUTPUT} && echo >> ${OUTPUT}
|
||||||
cat src/Model.js >> ${OUTPUT} && echo >> ${OUTPUT}
|
cat src/Model.js >> ${OUTPUT} && echo >> ${OUTPUT}
|
||||||
cat src/main.js >> ${OUTPUT} && echo >> ${OUTPUT}
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
@ -37,6 +37,7 @@
|
||||||
</div>
|
</div>
|
||||||
<script src="js/three.min.js"></script>
|
<script src="js/three.min.js"></script>
|
||||||
<script src="js/OrbitControls.js"></script>
|
<script src="js/OrbitControls.js"></script>
|
||||||
|
<script src="js/obja.js"></script>
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -5,7 +5,7 @@ animate();
|
||||||
|
|
||||||
function init() {
|
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 = new Loader(url, 1024, 20);
|
||||||
loader.start(function(elements) {
|
loader.start(function(elements) {
|
|
@ -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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue