Update, docs
This commit is contained in:
		
							parent
							
								
									3328393f1d
								
							
						
					
					
						commit
						4b8760c3e3
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,2 +1 @@ | ||||
| assets | ||||
| js/main.js | ||||
|  | ||||
							
								
								
									
										3
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								Makefile
									
									
									
									
									
								
							| @ -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
									
								
							
							
						
						
									
										127
									
								
								README.md
									
									
									
									
									
										Normal 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. | ||||
| @ -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> | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										313
									
								
								js/obja.js
									
									
									
									
									
										Normal 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++; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user