Comme nous l’avons mentionné dans un article de blog précédent, une production CG peut être représentée comme une structure de graphe. Un film est composé de plans qui sont générés à partir de fichiers de scènes, eux-mêmes constitués d’éléments liés par des relations. Néanmoins, lorsque nous stockons des données de production dans une base de données, nous avons tendance à utiliser une description « à plat » des données. Et lorsqu’il est temps de choisir une base de données, le choix le plus courant est de s’appuyer sur des bases de données relationnelles.
Utiliser une base de données relationnelle est un bon choix : c’est sûr et cela fait le travail correctement. Mais, aujourd’hui, quelques technologies de bases de données proposent de stocker vos données directement sous forme de graphes. Au départ, elles sont surtout utilisées pour traiter des réseaux sociaux ou des cas d’usage liés au secteur bancaire. Mais ce n’est pas une suprise : elles ont attiré l’attention de nombreux Directeurs Techniques et Développeurs issus de studios CG. Avec l’intérêt croissant pour les bases de données de graphes, nous avons décidé de les examiner de plus près.
L’information d’un graphe vous rendra plus agile. Le stockage de graphe permet d’enregistrer les dépendances de l’ensemble de vos assets et de définir les versions des éléments utilisés dans un plan. Et parce que les graphes stockés sont dirigés, vous pouvez facilement calculer une séquence d’opérations pour construire ou reconstruire un élément de la scène. Cela signifie plus de réactivité lorsque le réalisateur veut essayer de nouvelles choses.
Maintenant que nous avons une bonne raison d’utiliser des bases de données de graphes, nous allons jeter un œil aux principales bases de données open source disponibles sur le marché.
Exemple de cas d’usage
Pour explorer ces bases de données, nous proposons d’implémenter le graphe de données de l’animation des props décrite dans notre article précédent nommé CG production as a Graph. L’approche consistera à stocker les étapes nécessaires pour construire les props et à les inclure dans un plan donné.
Le plus courant, avec un graphe, est d’obtenir tous les impacts d’un changement sur un élément donné. Pour l’illustrer, nous allons effectuer une requête qui récupère les éléments impactés par le changement sur le mesh des props.
Nous fournirons des extraits Python pour montrer comment utiliser chaque base de données. Ensuite, nous lancerons un benchmark rapide. Nous comparerons le temps nécessaire pour exécuter 10 000 fois notre requête d’exemple sur un CPU i7–6700 @ 3.40GHz . Notez que ce benchmark inclut le client Python : nous considérons que vous n’utiliserez la base de données que via celui-ci. C’est pourquoi nous l’incluons dans nos mesures.
Principales bases de données
Les principales bases de données que nous allons étudier sont les suivantes :
Cayley
Cayley est une base de données de graphes distribuée par Google, écrite en Go. Elle semble prometteuse sur de nombreux aspects (backend configurable, communauté active), mais pour l’instant la documentation est presque inexistante. Quoi qu’il en soit, voyons ce que l’on peut faire avec.
Commencez par télécharger les binaires liés à votre plateforme, initialiser la base de données et lancer le serveur http qui nous permettra d’exécuter des requêtes. L’initialisation de la base de données ne signifie pas que vous devez fournir des données : il s’agit simplement de créer les fichiers de la base de données../cayley init -db bolt -dbpath /tmp/testdb
./cayley http --dbpath=/tmp/testdb --host 0.0.0.0 --port 64210
Vous pouvez remarquer ici qu’une autre technologie de base de données est impliquée (Bolt). C’est parce que Cayley est une couche au-dessus d’une base de données existante. Vous pouvez soit utiliser des stores clé-valeur traditionnels, soit une base de données relationnelle en backend.
Passons maintenant au code du client Python. Nous voulons stocker l’ensemble de nos assets, scènes, plans et leurs relations. Pour y parvenir, nous devons installer le driver Python :pip install pyley
Cayley est basé sur le concept de triplet. Tout est un sommet lié à un autre : le triplet est composé de trois sommets : les deux éléments que l’on souhaite relier et le sommet de lien (une sorte d’arête). Vous pouvez ajouter une étiquette sur chaque triplet, donc dans Cayley le terme pour cette structure de données est « quads ».
Malheureusement, le client Python n’est pas complet et ne supporte pas la création de Quad. Nous devons donc créer nos quads via des requêtes, un client HTTP Python standard (Cayley fournit une API REST) :def create_quad(quad):
path = “http://localhost:64210/api/v1/write"
return requests.post(path, json=[quad])
Passons maintenant à la création des quads :quads = [
{
“subject”: “props1-concept”,
“predicate”: “dependencyof”,
“object”: “props1-texture”
},
{
“subject”: “props1-concept”,
“predicate”: “dependencyof”,
“object”: “props1-mesh”
},
{
“subject”: “props1-texture”,
“predicate”: “dependencyof”,
“object”: “props1-model”
},
{
“subject”: “props1-mesh”,
“predicate”: “dependencyof”,
“object”: “props1-model”
},
{
“subject”: “props1-mesh”,
“predicate”: “dependencyof”,
“object”: “props1-rig”
},
{
“subject”: “props1-mesh”,
“predicate”: “dependencyof”,
“object”: “props1-keys”
}
{
“subject”: “props1-rig”,
“predicate”: “dependencyof”,
“object”: “props1-keys”
},
{
“subject”: “props1-model”,
“predicate”: “dependencyof”,
“object”: “shot1-image-sequence”
},
{
“subject”: “props1-keys”,
“predicate”: “dependencyof”,
“object”: “shot1-image-sequence”
}
]for quad in quads:
create_quad(quad)
C’est tout. Comme vous pouvez le voir, nous avons déjà stocké toutes nos données et défini des relations entre elles. Si vous créez à nouveau des quads similaires, rien ne changera et il n’y aura pas de doublons.
Passons maintenant à notre requête sur l’impact d’un changement de rig sur la production :from pyley import CayleyClient, GraphObject
client = CayleyClient("http://localhost:64210", "v1")graph = GraphObject()
query = graph.V(“props1-mesh”)
.Out()
.All()
Pour obtenir les données souhaitées, nous devions préciser quel sommet (ici notre texture) nous voulons étudier l’impact. Ensuite, il suffit de demander le côté « sortant » du sommet dont la texture fait partie. Vous pouvez enchaîner l’appel en fonction de la profondeur d’impact que vous voulez étudier. Une traversée récursive est disponible, mais le client Python ne l’implémente pas encore. Enfin, nous avons réalisé nos tests de performance. Il a fallu 50 secondes pour exécuter dix mille fois cette requête.
L’interface de visualisation ne fonctionne pas très bien et n’est pas très intuitive à utiliser. C’est dommage, car Neo4j et Arango disposent d’interfaces qui fonctionnent et qui permettent d’afficher votre graphe.
Cayley est une base de données très simple. Avec un seul concept, la représentation sous forme de quad, elle permet de représenter nos données. Les requêtes sont aussi très faciles et reposent sur un langage de requêtes de graphes standard comme Gremlin (vous pouvez choisir votre langage de requêtes préféré). Malheureusement, le projet manque encore de documentation et le client Python est incomplet. C’est pourquoi, malgré sa conception propre et simple, nous ne pouvons pas recommander d’utiliser Cayley en production.
Neo4j
Neo4j est la solution la plus mature. L’entreprise derrière propose une solution d’entreprise convaincante pour le support et des fonctionnalités supplémentaires (monitoring, sauvegardes, requêtes améliorées…). C’est un gros avantage si vous devez vous sentir très en sécurité en raison de contrats difficiles avec vos clients. Mais pour commencer, nous recommandons d’utiliser l’édition communautaire. C’est cette version que nous allons couvrir dans cet article.
Comme nous ne faisons qu’expérimenter, nous allons utiliser le Docker officiel pour jouer avec Neo4j :
docker run \ --publish=7474:7474 --publish=7687:7687 \ --volume=$HOME/neo4j/data:/data \ neo4jNous pouvons maintenant installer le driver Python :pip install neo4j-driver
Première chose : initialisons la connexion avec la base de données et la session de requête. Au début, on vous demandera de définir un mot de passe ; vous pouvez le faire via la dernière ligne de l’extrait ci-dessous :from neo4j.v1 import GraphDatabase, basic_authdriver = GraphDatabase.driver(
"bolt://localhost:7687",
auth=basic_auth("neo4j", "tests")
)
session = driver.session()
# session.run("CALL dbms.changePassword('tests')")
Ensuite, ajoutons des helpers pour créer des nœuds asset, des nœuds plan et des arêtes de relation. Le client Python ne fournit pas une API très solide : il permet simplement d’exécuter des requêtes directement avec le langage interne de Neo4j appelé Cypher. Il y a une commande CREATE, mais nous utiliserons MERGE car elle agit comme CREATE si elle n’existe pas :def create_asset(name):
session.run(
"MERGE (a:Asset { name: $name })",
name=name
)def create_shot(name):
session.run(
"MERGE (a:Shot { name: $name })",
name=name
)def create_relation(asset1, asset2):
session.run(
"MATCH (a:Asset { name: $asset1 }), (b:Asset { name: $asset2 })"
"MERGE (a)-[r:ELEMENT_OF]->(b)",
asset1=asset1, asset2=asset2
)def create_casting(asset, shot):
session.run(
"MATCH (a:Asset { name: $asset }), (b:Shot { name: $shot })"
"MERGE (a)-[r:CASTED_IN]->(b)",
asset=asset, shot=shot
)
Comme vous pouvez le voir, la syntaxe est facile à lire et à apprendre. On peut ajouter autant de champs que l’on veut sur un seul nœud.
Maintenant que nous avons nos fonctions, remplissons notre graphe :create_asset("Props 1 concept")
create_asset("Props 1 mesh")
create_asset("Props 1 texture")
create_asset("Props 1 rig")
create_asset("Props 1 model")
create_asset("Props 1 keys")
create_shot("Shot 1")create_relation("Props 1 concept", "Props 1 texture")
create_relation("Props 1 concept", "Props 1 mesh")
create_relation("Props 1 mesh", "Props 1 model")
create_relation("Props 1 texture", "Props 1 model")
create_relation("Props 1 mesh", "Props 1 rig")
create_relation("Props 1 mesh", "Props 1 keys")
create_relation("Props 1 rig", "Props 1 keys")create_casting("Props 1 model", "Shot 1")
create_casting("Props 1 keys", "Shot 1")
À présent, profitons du langage de requêtes expressif pour réaliser notre traversée. Notez l’étoile à l’intérieur de la flèche. Cela signifie que la traversée ira sur tous les nœuds tant qu’il y a des connexions sortantes. result = session.run(
"MATCH (:Asset { name: 'Props 1 mesh' })-[*]->(out)"
"RETURN out.name as name"
)for record in result:
print("%s" % record["name"])session.close()
Nous y sommes ! Les enregistrements de résultat sont faciles à afficher et à analyser. Ce sont des dictionnaires Python contenant les champs spécifiés lors de la création. Exécuter dix mille fois notre requête a duré 3,5 secondes (cela passe à 17 secondes si vous ouvrez/fermez la session à chaque fois).

Dans l’ensemble, Neo4j est complet, fait le travail correctement et est rapide par rapport aux autres. Son langage de requêtes puissant et ses nombreuses fonctionnalités vous permettront de couvrir la plupart des cas d’usage courants que vous aurez avec votre graphe. Le client Python officiel est un peu léger, mais la communauté propose une alternative intéressante avec un client construit comme un ORM. Enfin, la base de données existe depuis longtemps et l’entreprise derrière est très active. Ainsi, Neo4j devient le choix le plus sûr de cette revue.
NB : voici un retour d’expérience réel sur Neo4j.
Avec ArangoDB
ArangoDB est une base de données polyvalente qui permet de stocker des documents et des graphes. Récemment, elle a gagné en popularité : c’est la raison pour laquelle nous l’avons incluse dans le test. Elle propose des fonctionnalités pratiques, comme un déploiement facile sur une infrastructure cloud, ainsi que des helpers pour construire des API REST. Mais dans cet article, nous allons nous concentrer sur le stockage de graphes et sur son système de requêtes.
Passons au code ! Pour nos tests, nous devons d’abord avoir une instance Arango en cours d’exécution. Utilisons à nouveau Docker pour la lancer :docker run -p 8529:8529 -e ARANGO_ROOT_PASSWORD=openSesame arangodb/arangodb:3.2.1
Puis, installons le client Python :pip install python-arango
À présent, on peut écrire notre script Python : la première étape consiste à initialiser notre base de données :from arango.client import ArangoClientclient = ArangoClient(username='root', password='openSesame')
db = client.create_database('cgproduction')
Comme vous pouvez le voir, la création de la base de données est très simple. Le seul problème est qu’elle déclenche une exception si la base de données existe déjà. Cela signifie que si vous voulez obtenir l’idempotence avec votre script, vous devrez écrire votre propre méthode « get or create ». C’est la même chose pour chaque création que nous ferons dans la suite. Soyez prêt à enrichir ce driver Python.
L’étape suivante consiste à définir notre graphe et à configurer les collections qui stockeront les informations de sommets et d’arêtes :dependencies = db.create_graph('dependencies')shots = dependencies.create_vertex_collection('shots')
assets = dependencies.create_vertex_collection('assets')casting = dependencies.create_edge_definition(
name='casting',
from_collections=['assets'],
to_collections=['shots']
)
elements = dependencies.create_edge_definition(
name='element',
from_collections=['assets'],
to_collections=['assets']
)
Le stockage de graphe d’ArangoDB repose sur son propre système de stockage de documents. Chaque sommet est stocké comme une entrée json dans une collection. Les arêtes sont un peu différentes. Elles sont stockées de manière similaire, mais la définition de la collection nécessite plus d’informations : la collection du sommet « intérieur » et celle du sommet « extérieur ». Les arêtes sont toujours dirigées.
Maintenant que notre base de données est correctement configurée, nous pouvons ajouter nos données :# Insérer des sommets
assets.insert(
{'_key': 'props1-concept', 'name': 'Props 1 Concept'})
assets.insert(
{'_key': 'props1-texture', 'name': 'Props 1 Texture'})
assets.insert(
{'_key': 'props1-mesh', 'name': 'Props 1 Mesh'})
assets.insert({'_key': 'props1-rig', 'name': 'Props 1 Rig'})
assets.insert({'_key': 'props1-model', 'name': 'Props 1 Model'})
assets.insert({'_key': 'props1-keys', 'name': 'Props 1 Keys'})
shots.insert(
{'_key': 'shot1-image-sequence',
'name': 'Shot 1 Image sequence'})# Insérer des arêtes
elements.insert(
{'_from': 'assets/props1-concept',
'_to': 'assets/props1-texture'})
elements.insert(
{'_from': 'assets/props1-concept',
'_to': 'assets/props1-mesh'})
elements.insert(
{'_from': 'assets/props1-texture',
'_to': 'assets/props1-model'})
elements.insert(
{'_from': 'assets/props1-mesh',
'_to': 'assets/props1-rig'})
elements.insert(
{'_from': 'assets/props1-mesh',
'_to': 'assets/props1-model'})
elements.insert(
{'_from': 'assets/props1-mesh',
'_to': 'assets/props1-keys'})
elements.insert(
{'_from': 'assets/props1-rig',
'_to': 'assets/props1-keys'})
casting.insert(
{'_from': 'assets/props1-model',
'_to': 'shots/shot1-image-sequence'})
casting.insert(
{'_from': 'assets/props1-keys',
'_to': 'shots/shot1-image-sequence'})
Une fois nos données correctement importées, nous pouvons passer à notre requête :traversal_results = dependencies.traverse(
start_vertex=’assets/props1-mesh’,
direction=’outbound’
)for result in traversal_results[“vertices”]:
print(result[“name”])
Avec cette requête simple, nous obtenons tout l’impact d’une modification du mesh des props 1. Le résultat est facile à analyser et la requête est configurable (par exemple, vous pouvez choisir entre une traversée en profondeur d’abord et une traversée en largeur d’abord).
Arango fournit un objet de traversée qui vous permet de construire des chemins particuliers. Certains helpers sont aussi disponibles, comme la recherche de chemin le plus court ou la récupération de la longueur de chemin. Cela devrait couvrir la plupart de vos besoins en matière d’interrogation de graphes.
Enfin, vous pouvez visualiser votre graphe dans l’interface web d’Arango :

Dans l’ensemble, la base de données ArangoDB et le client Python sont simples à comprendre et bien documentés. Ils fournissent de nombreux helpers pour interagir avec notre graphe, et les outils de visualisation rendent les choses encore plus faciles. Mais elle semble plus lente que neo4j. Exécuter notre requête 10 000 fois a pris 26 secondes. Malgré ces résultats, c’est toujours notre base de données préférée de ce test. Arango est très conviviale pour les développeurs. C’est le meilleur choix pour expérimenter rapidement avec des bases de données de graphes. Et comme l’entreprise derrière semble très active, cela semble aussi être un choix sûr pour une utilisation en production.
OrientDB
OrientDB existe depuis un moment déjà (depuis 2010). Mais comme les retours à son sujet sont très mauvais (voir aussi les commentaires), nous avons décidé de ne pas couvrir cette base de données dans cet article. C’est trop risqué à utiliser dans un environnement de production CG.
Alternatives
Il existe encore des alternatives. En bidouillant une base de données traditionnelle, vous pouvez obtenir des fonctionnalités similaires à celles d’une base de données de graphes. Une option consiste à utiliser Postgres avec ses jointures récursives. Cela vous permettra de couvrir des cas d’usage simples de traversée de graphe.
Une autre option, qui semble très intéressante si vous voulez pouvoir faire des recherches floues, consiste à utiliser Elastic Search et à stocker tous les sommets et toutes les arêtes comme des documents JSON (approche similaire à ArangoDB). Lisez cet article complet pour en savoir plus sur le sujet.
Visualisation
Avoir des données de graphe, c’est super, mais vous voudrez peut-être construire des outils qui affichent vos données à un moment donné (et en dehors des UI intégrées).
Il existe deux bonnes bibliothèques pour Qt qui permettent de construire facilement des graphes :
- ZodiacGraph : une puissante bibliothèque C++ qui est rapide et flexible.
- Nodz : une bibliothèque Python facile à utiliser.
Une autre option consiste à utiliser des bibliothèques JavaScript pour des applications dans le navigateur ou Electron. Voici quelques exemples :
- SigmaJS : une bibliothèque rapide et bien documentée
- Cytoscape : polyvalente et robuste.
- d3.js : plus difficile à utiliser, mais sans limites.
Pour conclure
D’après notre étude, ArangoDB semble être la base de données la plus conviviale pour l’utilisateur, et son aspect « stockage de documents » facilitera la gestion de vos données de production. Mais c’est encore une base de données jeune. Si vous avez besoin de vitesse, ou s’il y a beaucoup d’argent en jeu, et si vous recherchez un choix plus sûr, optez pour Neo4j, qui fait le travail correctement et semble plus robuste. Enfin, Cayley semble prometteuse sur de nombreux aspects grâce à son excellent design, et pourrait être le meilleur choix pour compléter une base de données relationnelle déjà existante, mais elle est encore trop peu documentée et trop jeune pour être utilisée en production. Donc, pour résumer : essayez d’abord ArangoDB !
La question de savoir quels problèmes la représentation et le stockage par graphe résolvent pour les pipeline TD reste ouverte. Le principal cas d’usage pour nous est de générer facilement la séquence d’actions nécessaire pour reconstruire un plan lorsqu’un changement survient. L’autre est de fournir facilement une représentation de la production sur laquelle les gens peuvent discuter.
Nous espérons que vous apprécierez cet article. Nous sommes encore très novices sur les bases de données de graphes. Nous serions ravis de connaître votre avis et de lire votre expérience de production avec ces technologies : les commentaires sont les bienvenus !
Ce blog est dédié au pipeline CG et à la gestion de production. Si vous êtes intéressé par les bases de données de graphes pour des productions CG, vous apprécierez probablement tous nos articles. Lisez notre premier article de blog pour en savoir plus !



