Épreuve pratique · 2026 · Sujet 9

Bac NSI pratique 2026 sujet 9 : Objets 3D, arêtes et références partagées

Un fichier OBJ décrit un objet par son nom (o), ses sommets (v) et ses faces (f). Exemple : quatre sommets du plan z=0, puis f 1 2 3 4 décrivent une face carrée. Dans les classes Python fournies, les faces contiennent en réalité des références vers des instances de Sommet, malgré la description initiale en termes d’indices. Cette différence est centrale pour comprendre le défaut de changement d’échelle.

Dans ce sujet

Un corrigé pédagogique pour comprendre et justifier vos réponses. Les conseils de rédaction ne constituent pas un barème officiel détaillé.

Application

Objets 3D, arêtes et références partagées

Un fichier OBJ décrit un objet par son nom (o), ses sommets (v) et ses faces (f). Exemple : quatre sommets du plan z=0, puis f 1 2 3 4 décrivent une face carrée. Dans les classes Python fournies, les faces contiennent en réalité des références vers des instances de Sommet, malgré la description initiale en termes d’indices. Cette différence est centrale pour comprendre le défaut de changement d’échelle.

o cube
v 0.0 0.0 0.0
v 0.0 1.0 0.0
v 1.0 1.0 0.0
v 1.0 0.0 0.0
f 1 2 3 4

Le sujet utilise des coordonnées entières et une unité géométrique de 1 mm. Le volume est estimé par le cube de la longueur de la plus longue arête. Ce n’est pas, pour un objet quelconque, une garantie de véritable cube englobant ni un calcul exact du volume : on respecte ici cette approximation imposée. L’affichage utilise matplotlib.

Le dossier comporte le sujet et les fichiers listés dans les téléchargements. Les codes de départ sont reproduits ci-dessous. Travaillez sur une copie. Les appels professeur signalés dans les questions servent à présenter votre démarche ; vous pouvez aussi demander de l’aide en cas de difficulté.

Code fourni : Face.py
class Face:

    """
    Représente une face d'un objet 3D.
    """

    def __init__(self, sommets):
        self.sommets = sommets
Code fourni : Imprimante3D.py
from Objet3D import Objet3D

#############################################################################
# Variables et fonctions fournies pour la question 3                        #
#############################################################################


class Imprimante3D:
    def __init__(self, remplissage, vitesse_extrusion):
        self.remplissage = remplissage
        self.vitesse_extrusion = vitesse_extrusion

    #############################################################################
    #  Écrire le code de la méthode estimation_impression de la question 3      #
    #############################################################################

    def estimation_impression(self, objet):
        pass


rhombi = Objet3D()

# Définition des 24 sommets
rhombi.ajouter_sommet(1, 1, 3)    # s0
rhombi.ajouter_sommet(1, 1, -3)   # s1
rhombi.ajouter_sommet(1, -1, 3)   # s2
rhombi.ajouter_sommet(1, -1, -3)  # s3
rhombi.ajouter_sommet(-1, 1, 3)   # s4
rhombi.ajouter_sommet(-1, 1, -3)  # s5
rhombi.ajouter_sommet(-1, -1, 3)  # s6
rhombi.ajouter_sommet(-1, -1, -3)  # s7
rhombi.ajouter_sommet(1, 3, 1)    # s8
rhombi.ajouter_sommet(1, 3, -1)   # s9
rhombi.ajouter_sommet(1, -3, 1)   # s10
rhombi.ajouter_sommet(1, -3, -1)  # s11
rhombi.ajouter_sommet(-1, 3, 1)   # s12
rhombi.ajouter_sommet(-1, 3, -1)  # s13
rhombi.ajouter_sommet(-1, -3, 1)  # s14
rhombi.ajouter_sommet(-1, -3, -1)  # s15
rhombi.ajouter_sommet(3, 1, 1)    # s16
rhombi.ajouter_sommet(3, 1, -1)   # s17
rhombi.ajouter_sommet(3, -1, 1)   # s18
rhombi.ajouter_sommet(3, -1, -1)  # s19
rhombi.ajouter_sommet(-3, 1, 1)   # s20
rhombi.ajouter_sommet(-3, 1, -1)  # s21
rhombi.ajouter_sommet(-3, -1, 1)  # s22
rhombi.ajouter_sommet(-3, -1, -1)  # s23

# Faces carrées principales (axiales)
rhombi.ajouter_face([0, 2, 6, 4])     # Z+
rhombi.ajouter_face([1, 5, 7, 3])     # Z-
rhombi.ajouter_face([16, 17, 19, 18])  # X+
rhombi.ajouter_face([20, 22, 23, 21])  # X-
rhombi.ajouter_face([8, 12, 13, 9])   # Y+
rhombi.ajouter_face([10, 11, 15, 14])  # Y-

# Faces carrées de jonction (arêtes)
rhombi.ajouter_face([0, 16, 18, 2])   # Z+/X+
rhombi.ajouter_face([4, 6, 22, 20])   # Z+/X-
rhombi.ajouter_face([1, 3, 19, 17])   # Z-/X+
rhombi.ajouter_face([5, 21, 23, 7])   # Z-/X-
rhombi.ajouter_face([0, 4, 12, 8])    # Z+/Y+
rhombi.ajouter_face([2, 10, 14, 6])   # Z+/Y-
rhombi.ajouter_face([1, 9, 13, 5])    # Z-/Y+
rhombi.ajouter_face([3, 7, 15, 11])   # Z-/Y-
rhombi.ajouter_face([8, 16, 17, 9])   # Y+/X+
rhombi.ajouter_face([12, 20, 21, 13])  # Y+/X-
rhombi.ajouter_face([10, 18, 19, 11])  # Y-/X+
rhombi.ajouter_face([14, 22, 23, 15])  # Y-/X-

# Faces triangulaires (sommets)
rhombi.ajouter_face([0, 8, 16])  # X+Y+Z+
rhombi.ajouter_face([4, 12, 20])  # X-Y+Z+
rhombi.ajouter_face([2, 10, 18])  # X+Y-Z+
rhombi.ajouter_face([6, 14, 22])  # X-Y-Z+
rhombi.ajouter_face([1, 17, 9])  # X+Y+Z-
rhombi.ajouter_face([5, 13, 21])  # X-Y+Z-
rhombi.ajouter_face([3, 19, 11])  # X+Y-Z-
rhombi.ajouter_face([7, 15, 23])  # X-Y-Z-

# rhombi.afficher() # à décommenter pour afficher le rhombicuboctaèdre

imprimante = Imprimante3D(20, 1.2)
print(imprimante.estimation_impression(rhombi))
Code fourni : Objet3D.py
from Sommet import Sommet
from Face import Face
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection


class Objet3D:

    """
    Représente un objet 3D composé de sommets, de faces et d'un nom.
    """

    def __init__(self):
        """
        Initialise un objet 3D vide.
        """
        self.sommets = []
        self.faces = []
        self.nom = ""

    def ajouter_sommet(self, x, y, z):
        """
        Ajoute un sommet à l'objet 3D.
        """
        self.sommets.append(Sommet(x, y, z))

    def ajouter_face(self, liste_sommets):
        """
        Ajoute une face à l'objet 3D.
        """
        self.faces.append(Face([self.sommets[i] for i in liste_sommets]))

    def __str__(self):
        """
        Renvoie une représentation textuelle de l'objet 3D.
        """
        return str({'nom': self.nom, 'sommets': len(self.sommets), 'faces': len(self.faces)})

    def afficher(self):
        """
        Affiche l'objet 3D à l'aide de matplotlib.
        """
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')

        f = []
        for face in self.faces:
            x = [(s.x, s.y, s.z) for s in face.sommets]
            f.append(x)
        mesh = Poly3DCollection(f, alpha=0.4, edgecolor='black')
        ax.add_collection3d(mesh)
        plt.show()

    #############################################################################
    # Méthode à modifier de la question 5                                       #
    #############################################################################
    def transformer(self, rapport):
        """
        Applique une transformation d'échelle à l'objet 3D en modifiant directement ses sommets.
        """
        sommets = []
        for sommet in self.sommets:
            sommets.append(
                Sommet(sommet.x * rapport,
                       sommet.y * rapport, sommet.z * rapport))
        self.sommets = sommets

    #############################################################################
    # Écrire le code de la méthode sommets_adjacents de la question 2           #
    #############################################################################

    def sommets_adjacents(self, s1, s2):
        pass

    def longueur_plus_longue_arete(self):
        max_longueur = 0
        for s1 in self.sommets:
            for s2 in self.sommets:
                if self.sommets_adjacents(s1, s2):
                    d = s1.distance(s2)
                    if d > max_longueur:
                        max_longueur = d
        return max_longueur

    def volume_cube_englobant(self):
        longueur_max = self.longueur_plus_longue_arete()
        return longueur_max ** 3


#############################################################################
# Cube pour tester votre méthode de la question 2                           #
#############################################################################

cube = Objet3D()
cube.ajouter_sommet(0, 0, 0)  # s0
cube.ajouter_sommet(1, 2, 2)  # s1
cube.ajouter_sommet(3, 3, 0)  # s2
cube.ajouter_sommet(2, 1, -2)  # s3
cube.ajouter_sommet(-2, 2, -1)  # s4
cube.ajouter_sommet(-1, 4, 1)  # s5
cube.ajouter_sommet(1, 5, -1)  # s6
cube.ajouter_sommet(0, 3, -3)  # s7
cube.ajouter_face([0, 1, 2, 3])
cube.ajouter_face([4, 5, 6, 7])
cube.ajouter_face([0, 1, 5, 4])
cube.ajouter_face([1, 2, 6, 5])
cube.ajouter_face([2, 3, 7, 6])
cube.ajouter_face([3, 0, 4, 7])
# cube.afficher() # à décommenter pour afficher le cube en 3d
Code fourni : Sommet.py
import math


class Sommet:

    """
    Représente un sommet (point) dans l'espace 3D.
    """

    def __init__(self, x, y, z):
        """
        Initialise un sommet avec ses coordonnées.
        """
        self.x = x
        self.y = y
        self.z = z

    #############################################################################
    # Écrire le code de la méthode distance de la question 1                    #
    #############################################################################

    def distance(self, s):
        return ((s.x-self.x)**2+(s.y-self.y)**2+(s.z-self.z)**2)**0.5


#############################################################################
# Programme pour tester votre méthode de la question 1                      #
#############################################################################
s1 = Sommet(0, 0, 0)
s2 = Sommet(3, 4, 0)

Question 1

#

Écrire la méthode distance de Sommet : renvoyer la distance euclidienne entre l’instance courante et un autre Sommet. On utilise la racine carrée de la somme des carrés des écarts de coordonnées.

Indice

Trois différences, trois carrés, une seule racine finale.

Comprendre la correction
def distance(self, s):
    return math.sqrt((self.x-s.x)**2 + (self.y-s.y)**2 + (self.z-s.z)**2)
assert Sommet(0,0,0).distance(Sommet(3,4,0)) == 5.0
assert Sommet(1,2,3).distance(Sommet(1,2,3)) == 0.0

Le fichier livré contient déjà une implémentation équivalente avec **0.5. Il faut la comprendre et la vérifier, pas la remplacer pour le principe. Les carrés rendent la distance symétrique et éliminent le signe des différences.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)

Question 2

#

Écrire sommets_adjacents(s 1, s 2) dans Objet3D. Renvoyer True si les deux points constituent une arête, et False sinon. Les arêtes ne sont pas orientées.

PythonReconnaître une arête sans accepter une diagonaleÉcrivez votre solution et mettez-la à l’épreuve

Complétez sommets_adjacents(self, s1, s2). Une arête relie deux sommets consécutifs d’une face, y compris le dernier et le premier. Les arêtes sont non orientées. Comparez les coordonnées : deux instances différentes peuvent désigner un même point. Nous isolons la géométrie, sans l’affichage Matplotlib.

class Objet3D:
    def __init__(self):
        """
        Initialise un objet 3D vide.
        """
        self.sommets = []
        self.faces = []
        self.nom = ""

    def ajouter_sommet(self, x, y, z):
        """
        Ajoute un sommet à l'objet 3D.
        """
        self.sommets.append(Sommet(x, y, z))

    def ajouter_face(self, liste_sommets):
        """
        Ajoute une face à l'objet 3D.
        """
        self.faces.append(Face([self.sommets[i] for i in liste_sommets]))

    def sommets_adjacents(self, s1, s2):
        # Complétez cette méthode
        pass

Les cas de test proposés :

  • Première arête : Deux sommets consécutifs forment une arête.
  • Sens inverse : Une arête n’est pas un arc orienté.
  • Fermeture de la face : La dernière paire relie la fin et le début du contour.
  • Une diagonale : Appartenir à la même face n’est pas suffisant.
  • Même point, autre instance : Le sujet demande une relation entre points, pas seulement entre identités Python.
  • Aucune face : Sans contour, aucune arête ne peut être trouvée.
Indice

Lire la liste d’une face comme un contour fermé.

Comprendre la correction
def sommets_adjacents(self, s1, s2):
    def coord(s):
        return (s.x, s.y, s.z)
    a, b = coord(s1), coord(s2)
    for face in self.faces:
        n = len(face.sommets)
        for i in range(n):
            u = coord(face.sommets[i])
            v = coord(face.sommets[(i + 1) % n])
            if (a == u and b == v) or (a == v and b == u):
                return True
    return False

Dans une face polygonale, seules les paires consécutives et la paire dernier-premier sont des arêtes. Deux sommets de la même face peuvent former une diagonale : tester seulement leur appartenance à cette face serait faux. Le modulo referme le contour. La comparaison des coordonnées respecte la formulation du sujet ; elle accepte deux instances distinctes désignant le même point.

assert cube.sommets_adjacents(cube.sommets[0], cube.sommets[1])
assert cube.sommets_adjacents(cube.sommets[1], cube.sommets[0])
assert cube.sommets_adjacents(cube.sommets[0], cube.sommets[3])
assert not cube.sommets_adjacents(cube.sommets[0], cube.sommets[2])

Pour une face contenant quatre sommets dans l’ordre A, B, C, D, le parcours examine A-B, B-C, C-D puis D-A. L’expression (i + 1) % n ne sert qu’à refermer le contour lorsque i désigne le dernier sommet. Une paire A-C appartient bien à la face, mais elle traverse son intérieur : c’est une diagonale. Si elle était prise pour une arête, la longueur maximale et son cube seraient surestimés, ce qui fausserait la durée d’impression. Un petit défaut d’adjacence se propagerait donc jusqu’au résultat final.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)

Question 3

#

Écrire estimation_impression(objet) dans Imprimante3D : multiplier le volume par le taux de remplissage, puis diviser par la vitesse d’extrusion, pour obtenir des secondes.

Comprendre la correction
def estimation_impression(self, objet):
    assert 0 <= self.remplissage <= 1
    assert self.vitesse_extrusion > 0
    volume = objet.volume_cube_englobant() * self.remplissage
    return volume / self.vitesse_extrusion

Le cube fourni possède des arêtes de 3 mm ; son volume vaut 27 mm³. Avec un remplissage 0,2 et une vitesse 1,2 mm³/s, le temps estimé vaut 4,5 s. Le fichier fourni crée Imprimante3D(20,1.2), alors que le texte définit un taux entre 0 et 1 : pour 20%, il faut corriger l’appel en Imprimante3D(0.2,1.2). La formule ne doit pas compenser silencieusement cette incohérence.

imprimante = Imprimante3D(0.2, 1.2)
assert abs(imprimante.estimation_impression(cube) - 4.5) < 1e-10
Voir la question dans le sujet PDF, p. 4 (nouvel onglet)

Question 4

#

Afficher le cube avant et après transformer(2), analyser pourquoi sa dimension ne double pas à l’affichage, puis corriger transformer.

PythonAgrandir le sommet vu par toutes les facesÉcrivez votre solution et mettez-la à l’épreuve

Corrigez transformer(self, rapport). Multipliez x, y et z de chaque sommet existant par rapport. Les faces doivent continuer à désigner ces mêmes objets ; remplacer seulement self.sommets ne suffit pas. L’atelier vérifie les références et les coordonnées, sans ouvrir de fenêtre 3D.

class Objet3D:
    def __init__(self):
        """
        Initialise un objet 3D vide.
        """
        self.sommets = []
        self.faces = []
        self.nom = ""

    def ajouter_sommet(self, x, y, z):
        """
        Ajoute un sommet à l'objet 3D.
        """
        self.sommets.append(Sommet(x, y, z))

    def ajouter_face(self, liste_sommets):
        """
        Ajoute une face à l'objet 3D.
        """
        self.faces.append(Face([self.sommets[i] for i in liste_sommets]))

    def transformer(self, rapport):
        # Complétez cette méthode
        pass

Les cas de test proposés :

  • Une face voit le déplacement : Les listes de sommets et de faces doivent observer la même instance transformée.
  • Deux faces partagent un sommet : Un partage entre faces ne doit pas provoquer deux transformations du même sommet.
  • Origine et coordonnées négatives : Chaque axe et chaque signe sont conservés par le facteur d’échelle.
  • Transformation identité : Le facteur 1 conserve valeurs et identités ; la méthode agit en place.
  • Objet vide : L’absence de sommets ne demande aucune transformation.
Indice

Quelles instances sont lues par afficher ?

Comprendre la correction

La méthode crée de nouveaux Sommet dans self.sommets, mais chaque Face conserve des références aux anciens objets. afficher lit les coordonnées depuis les faces : elle voit donc les anciens sommets. La correction la plus simple modifie les objets existants, afin que toutes les références observent les mêmes coordonnées.

def transformer(self, rapport):
    for sommet in self.sommets:
        sommet.x *= rapport
        sommet.y *= rapport
        sommet.z *= rapport
cube.afficher()
ancien = cube.faces[0].sommets[1]
cube.transformer(2)
assert cube.sommets[1] is ancien
assert (ancien.x, ancien.y, ancien.z) == (2, 4, 4)
cube.afficher()

Doubler les longueurs multiplie le volume par 8, pas par 2. Contrôler les coordonnées et le volume est plus fiable que juger seulement la taille apparente dans une fenêtre 3D, dont les axes peuvent être réajustés. Recréer le cube avant chaque test pour éviter de multiplier plusieurs fois son échelle.

Dessinez deux flèches vers le même sommet : l’une depuis la liste de l’objet, l’autre depuis la liste d’une face. La version défaillante déplace seulement la première flèche vers un nouveau sommet agrandi. La seconde reste sur l’ancienne instance. La version corrigée conserve les deux flèches et change les coordonnées de l’objet qu’elles désignent. Un test avec is vérifie cette identité partagée ; un test de coordonnées vérifie la transformation numérique. Les deux sont nécessaires pour expliquer pourquoi le prochain affichage utilise enfin les nouvelles valeurs.

Repérer une référence périméeUn atelier pour expérimenter

Choisissez une réponse puis confrontez votre intuition à l’explication. Vous pouvez modifier vos choix.

Lire les réponses et leurs explications
  1. Remplacer self.sommets met-il à jour automatiquement chaque Face ?

    Non, les faces gardent leurs anciennes références.. Le lien porte sur l’objet, pas sur son emplacement dans la liste.

  2. Une face [0,1,2,3] contient-elle l’arête 0-2 ?

    Non, c’est une diagonale.. Les arêtes sont 0-1,1-2,2-3 et 3-0.

Appuyez votre explication sur les valeurs et les conditions exactes du programme.

Voir la question dans le sujet PDF, p. 4 (nouvel onglet)
Transformer le cube en gardant ses référencesUn atelier pour expérimenter

Changez le rapport et la stratégie. Le tableau compare le sommet 1 vu par la liste principale et par la première face. Les valeurs initiales sont celles du fichier.

Lire le résultat de l’expérience initiale

La liste et la face voient les mêmes coordonnées.

Les faces contiennent des références. Modifier un objet partagé est différent de remplacer une référence dans une seule liste.

Vuexyz
Liste principale244
Première face244

Pour comprendre un bug d’objet, dessinez qui référence quelle instance.

Revoir les notions de cet exercice

Du sujet à la méthode

Votre prochaine séance de révision

  • Tester les arêtes en excluant les diagonales.
  • Pour une transformation, vérifier données et références, pas uniquement l’affichage.

Retrouver ces notions dans d’autres sujets

Toutes les annales de NSI · Le guide pour préparer le bac NSI 2027

Énoncé : sujet 26_BCG_NSI_9 (PDF). Corrigé et explications pédagogiques proposés par Sofien.