Charger des fichiers .OBJ | WebGPU

Nous allons créer un chargeur .OBJ capable de lire et d'analyser des données 3D que nous utiliserons plus tard pour rendre un modèle 3D dans le navigateur.

Keywords: WebGPU, javascript, rendu 3D

By Carmen Cincotti  

❗ Cet article fait partie du projet WebGPU Cloth Simulation. Pour voir ce code, voyez ce repo dans GitHub 🧵

Avant de développer une simulation de tissu, j’aimerais développer une base de code solide à l’aide de WebGPU. C’est pourquoi j’aimerais commencer avec un chargeur de fichiers Wavefront .OBJ pour rendre un modèle 3D. De cette façon, nous pouvons rendre un modèle 3D rapidement ainsi que construire un moteur de rendu simple et robuste pour accomplir cette tâche. Une fois que nous avons une base solide, nous pouvons facilement mettre en œuvre la partie simulation de tissu.

Le fichier Wavefront .OBJ

.OBJ est un format de fichier contenant la description d’une géométrie 3D créée par la société Wavefront Technologies. La structure d’un tel fichier contient un ensemble :

  • de Sommets
  • de Normales
  • de Coordonnées de texture
  • de Faces.

Voyons un exemple. Une pyramide .obj est définie comme suit :

v 0 0 0 v 1 0 0 v 1 1 0 v 0 1 0 v 0.5 0.5 1.6 f 4// 1// 2// f 3// 4// 2// f 5// 2// 1// f 4// 5// 1// f 3// 5// 4// f 5// 3// 2//

En rendant ce fichier, on pourrait voir une pyramide qui ressemble à cette image :

Pyramid

Alors, la question se pose 🤔 :

Comment pourrions-nous charger ce format de fichier dans notre programme ?

On verra comment faire cela avec des fichiers .OBJ prêts à l’emploi afin de rendre des géométries complexes. C’est la beauté d’utiliser le travail d’un autre pour nous épargner le travail acharné. 💅

Comment trouver des fichiers .OBJ ?

J’utilise Google pour trouver les fichiers .OBJ. Cela dit, si je trouve un fichier qui me plaît, il me faut le charger dans un logiciel comme Blender pour plusieurs raisons :

  • Cohérence du format : Lorsqu’on utilise Google pour trouver des fichiers .OBJs, ils ont tous des petites particularités de format. Par exemple, ils pourraient définir des sommets et des faces avec ou sans des barres obliques. J’aimerais charger et exporter avec Blender pour assurer le format du contenu du fichier. 📁

  • Positionnement de la géométrie (optionnel) : Parfois, le modèle est positionné d’une manière que nous ne voulons pas. Corriger la position initiale dans Blender peut nous faire gagner du temps et du code.

Voyons un exemple. Pour ce projet, j’aimerais bien utiliser le célèbre lapin de Stanford. Le fichier se trouve là :

Stanford Bunny

Comment préparer une géométrie dans Blender

Après avoir téléchargé le fichier, il faut l’ouvrir dans un logiciel 3D comme Blender pour le vérifier. Immédiatement, nous pouvons voir un problème avec la position :

Bunny in Blender Step 1

J’aimerais centrer le lapin pour que son corps soit à l’origine. Pour ce faire, c’est assez simple. Voici ces étapes :

  1. Placez l’origine sur le lapin. Effectuez un clic droit, puis vous vous naviguez Set Origin > Origin to Geometry.

Bunny in Blender, rendered in Blender

  1. Déplacez le lapin à l’origine de la scène. Effectuez un clic droit, puis vous vous naviguez Snap > Selection to Cursor.

Bunny in Blender, snapping to origin

Le lapin est désormais centré à l’origine 🎯 :

Bunny in Blender, centered at origin

  1. Enfin, une bonne idée est de vérifier les normales associées avec le modèle. Nous pouvons refaire le calcul en entrant Edit Mode (en pressant TAB) dans Blender et en navigant à Mesh > Normals > Recalculate Outside :

Bunny in Blender, editing the normals

💡Comment voir visuellement les normales dans Blender

Pour voir les normales pour les vérifier, ouvrez le menu Overlays et cochez une case avec l’étiquette Normals :

Bunny in Blender, checking the normals

  1. Naviguez-vous à File > Export > Wavefront (.obj). Vous pouvez exporter le fichier en utilisant ces paramètres ci-dessous :

✅ Apply Modifiers
✅ Write Normals
✅ Include UVs
✅ Write Materials
✅ Triangulate Faces

Bunny in Blender, exporting to object file

Après avoir fait cela, vous devriez avoir un fichier .OBJ qui est prêt à être rendu dans le navigateur en utilisant WebGPU. 🥳

Le code WebGPU

💡Le code suppose désormais que le fichier a été préparé par Blender. Sinon, consultez la section précédente.

L’objectif est de stocker toutes nos données qui se trouvent dans le fichier .OBJ dans des tampons 🥅. Fort heureusement, les données sont facilement lisibles. J’ai conçu le système en deux parties :

  • Chargeur (Loader) - On charge le fichier et stocke son texte en mémoire pour qu’on puisse le traiter.
  • Analyseur (Parser) - Avec le texte stocké en mémoire, on peut analyser les lignes de texte et les stocker dans les tampons.

Chargeur

Regardons la fonction load():

async function load(filePath: FilePath): Promise<ObjFile> { const resp = await fetch(filePath); if (!resp.ok) { throw new Error( `ObjLoader could not fine file at ${filePath}. Please check your path.` ); } const file = await resp.text(); if (file.length === 0) { throw new Error(`${filePath} File is empty.`); } return file; }

L’idée de ce code est simplement de fetch le contenu d’un fichier, file, qui se trouve à un filePath. Je stocke mes fichiers sur mon disque dur, mais il est possible de demander les données en envoyant une requête HTTP.

Parseur

Voici la première partie du code :

parse(file: ObjFile): Mesh { const lines = file?.split("\n"); // Store what's in the object file here const cachedVertices: CacheArray<CacheVertice> = []; const cachedFaces: CacheArray<CacheFace> = []; const cachedNormals: CacheArray<CacheNormal> = []; const cachedUvs: CacheArray<CacheUv> = []; // Read out data from file and store into appropriate source buckets { for (const untrimmedLine of lines) { const line = untrimmedLine.trim(); // remove whitespace const [startingChar, ...data] = line.split(" "); switch (startingChar) { case "v": cachedVertices.push(data.map(parseFloat)); break; case "vt": cachedUvs.push(data.map(Number)); break; case "vn": cachedNormals.push(data.map(parseFloat)); break; case "f": cachedFaces.push(data); break; } } } ... Rest of code }

Cette partie consiste simplement à lire les données de la mémoire et à les stocker dans des tampons correspondants. Heureusement, chaque ligne de texte est étiquetée avec son type associé :

  • v - la position d’un sommet (vertex)
  • vt - coordonnées de texture (uv)
  • vn - le vecteur normal (normal)
  • f - la face (trois sommets qui forment un triangle)

Voici le reste du code :

... the code before // Use these intermediate arrays to leverage Array API (.push) const finalVertices: toBeFloat32[] = []; const finalNormals: toBeFloat32[] = []; const finalUvs: toBeFloat32[] = []; const finalIndices: toBeUInt16[] = []; // Loop through faces, and return the buffers that will be sent to GPU for rendering { const cache: Record<string, number> = {}; let i = 0; for (const faces of cachedFaces) { for (const faceString of faces) { // If we already saw this, add to indices list. if (cache[faceString] !== undefined) { finalIndices.push(cache[faceString]); continue; } cache[faceString] = i; finalIndices.push(i); // Need to convert strings to integers, and subtract by 1 to get to zero index. const [vI, uvI, nI] = faceString .split("/") .map((s: string) => Number(s) - 1); vI > -1 && finalVertices.push(...cachedVertices[vI]); uvI > -1 && finalUvs.push(...cachedUvs[uvI]); nI > -1 && finalNormals.push(...cachedNormals[nI]); i += 1; } } } return { vertices: new Float32Array(finalVertices), uvs: new Float32Array(finalUvs), normals: new Float32Array(finalNormals), indices: new Uint16Array(finalIndices), }; }

Ensuite, nous itérons sur les éléments du tableau faces afin de créer et de stocker les données dans les tampons finaux. Nous utilisons quelque chose appelé tampon d’index qui est un moyen d’éviter de stocker des données en double. Nous verrons comment dans un instant.

💡 La face d’un fichier .OBJ

La définition d’une face nous fournit les données associées aux trois sommets qui forment un triangle.

f v0/vt0/vn0 v1/vt1/vn1 v2/vt2/vn2

Revoyons les données de la pyramide d’avant :

v 0 0 0 v 1 0 0 v 1 1 0 v 0 1 0 v 0.5 0.5 1.6 f 4// 1// 2// f 3// 4// 2// f 5// 2// 1// f 4// 5// 1// f 3// 5// 4// f 5// 3// 2//

Comme je l’ai dit, chaque face est définie par trois sommets et chaque nombre représente un indice (non indexé à zéro) qui peut être utilisé pour indexer dans la liste d’un attribut spécifique. Par exemple : f 4// 1// 2// est formé avec des sommets qui ont les positions (0 1 0), (0 0 0), (1 0 0).

Nous pouvons imaginer s’il y avait les définitions de normales et de coordonnées de texture, on aurait pu voir les nombres comme celui-ci : f 4/1/3 1/2/2 2/4/3.

🤔 Une bonne idée est de regarder de près votre fichier qui est sorti de Blender.

Les types de tampons

Comme on a vu lors de la discussion où l’on a créé un triangle dans WebGPU, on utilise des tampons pour stocker des attributs à chaque sommet.

Plus précisément, on utilise un ou plusieurs Objets de tampon de sommets (Vertex Buffer Objects - VBOs) et un Objet tampon d’index (Index Buffer Object - IBO). Nous utilisons les indices dans l’IBO pour indexer dans un VBO pour éviter de stocker des données en double.

Voici un exemple :

Triangle that shares index data

Le sommet avec l’étiquette de 2 se trouve dans deux triangles (l’un formé par les sommets 1, 2, 3 et l’autre par les sommets 3, 2, 4). Définissons les données comme suit :

position_vbo = [ -1, 0, 0, #v1 1, 0, 0, #v2 0, 1, 0, #v3 2, 1, 0, #v4 ] color_vbo = [ 1, 0, 0, #v1 0, 0, 1, #v2 1, 1, 0, #v3 2, 1, 0, #v4 ] indices_ibo = [ 0, 1, 2, # triangle 1 2, 1, 3 # triangle 2 ]

Comme nous pouvons le voir, nous pouvons réutiliser les données dans position_vbo et color_vbo en utilisant des indices. Dans le code, nous construisons des tableaux, tels que positions qui servira d’attribut de sommet et donc vivre dans un VBO, et indices (qui serviront d’IBO) à utiliser dans l’application - que nous verrons dans une partie ultérieure.

La suite

Nous verrons la prochaine étape - comment rendre ces données 3D que nous avons chargées avec notre nouveau chargeur .OBJ et nous examinerons de plus près les fonctionnalités de rendu telles que les tests de profondeur.

Des ressources (en français et anglais)


Comments for Charger des fichiers .OBJ | WebGPU



Written by Carmen Cincotti, computer graphics enthusiast, language learner, and improv actor currently living in San Francisco, CA.  Follow @CarmenCincotti

Contribute

Interested in contributing to Carmen's Graphics Blog? Click here for details!