On se propose de construire une famille de formes gauches (courbes, surfaces, volumes, hypervolumes) engendrées initialement par l'application récursive d'une opération simple :
« construire le milieu de deux points »
Mais comment écrire le milieu de deux points, et au delà toute combinaison de points ? On sait combiner linéairement des vecteurs dans un espace vectoriel. Mais l'espace des points (appelé espace affine, sans métrique définie) ne possède pas toutes les propriétés d'un espace vectoriel même s'il lui ressemble et notamment, toutes les combinaisons linéaires de points ne sont pas valables. On peut par exemple définir la somme de deux vecteurs de façon indépendante du repère, mais on ne peut pas le faire pour deux points. En revanche, la demi–somme de deux points produit un point invariant dans un changement de repère et constitue de ce fait une combinaison linéaire pondérée valable.
De manière générale, on montre que toute combinaison linéaire de n points est valable si elle satisfait à la condition :
∑iki.Pi = 1, avec i € [0,n-1]Seront notamment correctes des expressions comme :
p = (p0 + p1)/2 p = 2.p0 – p1 p = (2.p0 + p1)/3 p = (p0 + 3.p1 + 3.p2 + p3)/8
La première retourne le milieu de deux points, la seconde le point symétrique de p1 par rapport à p0, la troisième le point au tiers du segment [p0p1], la dernière le milieu d'une cubique [p0,p1,p2,p3]. On notera que tout ceci n'est qu'une autre expression des théorèmes bien connus sur les barycentres. Ceci ayant été précisé, nous pouvons construire une première famille de 'formes multilinéaires récursives' construites avec un couple d'opérations, l'étendre par application d'une opération de « diagonalisation », et enfin proposer la définition générale des formes pascaliennes.
Tous les exemples seront construits dans l'environnement Sketchup, depuis la console Ruby, sur la base de scripts écrits dans un fichier texte "PF/pFormes.rb". La bibliothèque "PF/pFlibs.rb" écrite dans le langage Ruby embarqué dans Sketchup définit l'ensemble des pFormes. La syntaxe correspondante sera appelée SRpF (Sketchup/Ruby/pFormes).
Au sommaire de cette section :
L'application d'un couple d'opérations à un ensemble de points va nous permettre de générer naturellement les premières formes linéaires de la géométrie : des segments de droite, des facettes (gauches), des cubes (gauches), des hypercubes (gauches), et au-delà si nécessaire.
Les points de l'espace sont définis par des tableaux de nombres réels. Afin de préparer la construction des objets géométriques, on se propose de partitionner l'intervalle [0,1] en le divisant récursivement en deux :
0 1 2 3 4 [...,...] [0/8,1/8] : [...,...] [0/4,1/4] : : [...,...] : [1/8,2/8] : [...,...] [0/2,1/2] : : [...,...] : : [2/8,3/8] : : : [...,...] : [1/4,2/4] : : [...,...] : [3/8,4/8] : [...,...] [0,1] : [...,...] : [4/8,5/8] : : [...,...] : [2/4,3/4] : : : [...,...] : : [5/8,6/8] : : [...,...] [1/2,2/2] : [...,...] : [6/8,7/8] : : [...,...] [3/4,4/4] : [...,...] [7/8,8/8] [...,...]
En s'arrêtant à l'étape 3) et une fois les doublons éliminés, on obtient le résultat suivant :
[0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0]
Pour obtenir "automatiquement" ce résultat - et aller sans mal beaucoup plus loin - on écrit les lignes suivantes dans la console Ruby de Sketchup :
1. ma_partition = Partition.new( [0,1] ) 2. ma_partition.build( 3 ) 3. ma_partition.draw()
Pour information, la classe "Partition" est définie ci-dessous en cinq fonctions :
class Partition def initialize( tab ) @_in = tab @_last = @_in[@_in.length()-1] @_out = [] end def build( rec ) subdivision( @_in, rec ) @_out.push( @_last ) end def subdivision( tab, rec ) if (rec >0) m = middle( tab[0], tab[1] ) subdivision( [tab[0], m], rec-1 ) subdivision( [m, tab[1]], rec-1 ) else @_out.concat( tab ) @_out.pop() end end def middle( a, b ) return ( a+b )/2.0 end def draw() return @_out end end
Il faut noter que l'application du processus récursif pour une valeur infinie de "recursion" produit un ensemble infini et "dense" de points situés dans l'intervalle [0..1]. Un ensemble dense mais pas continu au sens mathématique du terme, car il n'est pas en bijection avec le corps R des nombres réels ; il suffit de penser aux valeurs 1/3 ou PI/4 qui, bien que dans contenues dans l'intervalle [0..1], ne lui appartiennent pas. Nous verrons que cela a une incidence sur la nature des formes géométriques que nous allons aborder.
La classe "Partition" est le prototype de la classe "PF" gérant la totalité des Formes Pascaliennes. Le code est disponible en fin de document. Son analyse sort du champ de ce document mais on retrouvera toujours dans les exemples présentés les trois phases principales "Création, Partition, Affichage" appliquées aux objets géométriques (segments, surfaces, volumes,...) du plus simple au plus complexe :
1. pForme = PF.new( tableau de points ) 2. pForme.build( recursion ) 3. pForme.draw( style )
Les combinaisons linéaires de points envisagées plus haut sont exprimables dans un espace affine de dimension quelconque Rn. Pour des raisons qui seront données un peu plus loin, nous opèrerons sur des points toujours définis dans R4, en utilisant la forme dite des coordonnées homogènes ou forme projective : [x,y,z,w], associant au point de R4 le point de R3 défini par [x/w,y/w,z/w]. Dans un premier temps, on prendra par défaut w=1 et on oubliera qu'on travaille dans l'espace R4. On le retrouvera dans la section sur les coniques et les NURBS.
Dans la syntaxe SRpF, un point sera créé et affiché ainsi :
def point( m = 90 ) pP = PF.new( [ m, m, m ] ) pP.draw( "P" ) end
Dans cet exemple, les coordonnées du point sont [90,90,90] :
Deux points p0 et p1 étant donnés, nous en construisons un troisième, pm, situé au milieu :
pm = (p0 + p1)/2
Noter que la somme des coefficients est: 1/2 + 1/2 = 1. Ce troisième point pm donne naissance à deux couples de points (p0,pm) et (pm,p1), et à l'irrésistible envie de recommencer à gauche et à droite pour obtenir les points au 1/4 et au 3/4 et ainsi de suite.
Comme nous l'avons déjà vu concernant la partition de l'intervalle unité, l'application récursive de ce processus pour une valeur infinie de la récursion produit un ensemble infini et "dense" de points situés entre les deux points p0 et p1, un "segment de droite" que nous désignerons par pL2 (les raisons de cette notation seront données plus loin dans le texte) et que nous écrirons :
pL2 = [ p0, p1 ]
Pour les mêmes raisons cet ensemble de points (pL2) n'est pas à proprement parler un segment de droite tel qu'on le définit habituellement en géométrie. Si le processus récursif amène bien à un ensemble de points dont le nombre est aussi grand qu'on le souhaite, avec une distance entre chaque point tendant bien vers zéro, l'ensemble obtenu n'est pas continu et n'est pas "réellement" un segment de droite.
On définit habituellement dans Rn un segment de droite reliant un couple de points (p0, p1) comme l'ensemble des points p satisfaisant à l'application linéaire bijective :
p(t) = (1-t)•p0 + t•p1, avec t dans l'intervalle [0,1] € R
Cet ensemble de points est infini et continu (comme R), et des points comme p(1/3) ou p(k) avec k = racine(2)/2, peuvent bien être atteints sur ce segment de droite ; on a vu que tel n'est pas le cas de pL2 qui est en relation bijective avec l'ensemble des partitions de l'unité (1/2, 1/4, 1/8,...). On a dit d'un tel ensemble qu'il est dense, et c'est bien cette propriété de densité qui nous intéresse, notamment parce qu'elle nous permettra de retrouver le concept indispensable de tangente. Nous y reviendrons plus longuement dans la section 2, et pour la suite nous conviendrons d'assimiler notre pL2 à un "vrai" segment de droite et les formes dérivées (surfaces, volumes,...) aux formes continues classiques de la géométrie.
Dans la syntaxe SRpF, un segment sera créé et affiché ainsi :
def segment( m = 90, finesse = 3 ) pL2 = PF.new( [ [-m,-m,-m ], [ m, m, m ] ] ) pL2.build( finesse ) pL2.draw( "P" ) end
Dans cet exemple, le segment relie deux points de coordonnées [-90,-90,-90] et [90,90,90] :
Soient deux segments pL2_0 et pL2_1 construits sur les points (p00,p01) et (p10,p11). Considérant le segment construit sur les milieux respectifs des extrémités des segments pL2_0 et pL2_1 :
pL2_M = [ q0, q1 ] où q0 = (p00 + p10)/2 et q1 = (p01 + p11)/2
Par analogie avec la construction du segment, l'application récursive de ce processus aux couples de segments à gauche (pL2_0,pL2_M) et à droite (pL2_M,pL2_1) produit un ensemble infini et dense de segments formant une portion de surface, un "quadrangle gauche" que nous désignerons par pS22 et que nous écrirons :
pS22 = [ pL2_0, pL2_1 ] = [ p00, p01], [p10, p11] ]
Comme pour le segment, cet ensemble de segments (pL2) n'est pas un quadrangle tel qu'on le définit habituellement en géométrie. Si le processus récursif amène bien à un ensemble de segments dont le nombre est aussi grand qu'on le souhaite, avec une distance entre chaque segment tendant bien vers zéro, l'ensemble obtenu n'est pas continu et n'est pas "réellement" une surface quadrangle. On définit habituellement dans Rn une surface quadrangle reliant un couple de segments ( [p00,p01], [p10,p11] ) comme l'ensemble des points p satisfaisant à l'application bilinéaire bijective :
p(u,v) = (1-u)•pL_0(v) + u•pL_1(v) avec pL_0(v) = (1-v)•p00 + v•p01 et pL_1(v) = (1-v)•p10 + u•p11 p(u,v) = (1-u)•( (1-v)•p00 + v•p01 ) + u•( (1-v)•p10 + u•p11 ), p(u,v) = (1-u)•(1-v)•p00 + u•(1-v)•p01 + (1-u)•v•p10 + u•v•p11, avec u,v dans l'intervalle [0,1] € R
Pour la suite nous conviendrons d'assimiler notre pS22 à un quadrangle classique de la géométrie.
Dans la syntaxe SRpF, un quadrangle sera créé et affiché ainsi :
def quadrangle( m = 90, finesse = [3,3] ) l2_0 = [ [ -m,-m, 0 ], [ m,-m, m ] ] l2_1 = [ [ -m, m, m ], [ m, m, 0 ] ] pS22 = PF.new( [ l2_0, l2_1 ] ) pS22.build( finesse ) pS22.draw( "S" ) end
Dans cet exemple, le quadrangle relie deux segments situés sur deux faces d'un cube de côtés [180,180,90] :
Soient deux quadrangles pS22_0 et pS22_1 construits sur les couples de segments (pL2_00,pL2_01) et (pL2_10,pL2_11). Considérons le quadrangle pS22_M construit sur les milieux respectifs des extrémités des segments. Par analogie avec la construction du quadrangle, l'application récursive de de ce processus aux couples de quadrangles en haut (pS22_0,pS22_M) et en bas (pS22_M,pS22_1) produit un ensemble infini et dense de quadrangles formant une portion de volume, un "cuboïde" que nous désignerons par pV222 et que nous écrirons :
pV222 = [ pS22_0, pS22_1 ] = [ [ pL2_00, pL2_01 ], [ pL2_10, pL2_11 ] ] = [ [ [p000, p001], [p010, p011] ], [ [p100, p101], [p110, p111] ] ]
Ici aussi, cet ensemble de quadrangles (pV222) n'est pas à proprement parler un volume tel qu'on le définit habituellement en géométrie. Si le processus récursif amène bien à un ensemble de quadrangles dont le nombre est aussi grand qu'on le souhaite, avec une distance entre chaque quadrangle tendant bien vers zéro, l'ensemble obtenu n'est pas continu et n'est pas "réellement" un volume. On définit habituellement dans Rn un cuboïde reliant un couple de quadrangles ( pS22_0, pS22_1 ) comme l'ensemble des points p satisfaisant à l'application trilinéaire bijective :
p(u,v,w) = (1-u)•pS22_0(v,w) + u•pS22_1(v,w), où pS22_0(v,w) = (1-u)•(1-v)•p000 + u•(1-v)•p001 + (1-u)•v•p010 + u•v•p011 et pS22_0(v,w) = (1-u)•(1-v)•p100 + u•(1-v)•p101 + (1-u)•v•p110 + u•v•p111 p(u,v,w) = (1-u)•(1-v)•(1-w)•p000 + ... + u•v•w•p111 avec u,v,w dans l'intervalle [0,1] € R
Pour la suite nous conviendrons d'assimiler notre pV222 à un classique cuboïde de la géométrie.
Dans la syntaxe SRpF, un cuboïde sera créé et affiché ainsi :
def cuboide( m = 90, finesse = [3,3,3] ) pL2_00 = PF.new( [ [ -m,-m, 0 ], [ m,-m, m ] ] ) pL2_01 = PF.new( [ [ -m, m, m ], [ m, m, 0 ] ] ) pS22_0 = PF.new( [ pL2_00.poles(), pL2_01.poles() ] ) pS22_1 = PF.new( pS22_0.poles() ) pS22_1.rotate( [0,0,15] ) # optionnel pS22_1.scale( [0.5,0.5,0.5] ) # optionnel pS22_1.translate( [0,0,m] ) # pV222 = PF.new( [ pS22_0.poles(), pS22_1.poles() ] ) pV222.build( finesse ) pV222.draw( "V" ) end
Dans cet exemple, le cube gauche est construit sur deux quadrangles :
Soient deux cubes gauches pV222_0 et pV222_1. L'application récursive d'un processus analogue aux précédents produit un ensemble infini et dense de cubes gauches formant une portion d'hypervolume, un "hypercube gauche" que nous désignerons par pH2222 et que nous écrirons :
pH2222 = [ pV222_0, pV222_1 ]
Dans la syntaxe SRpF, un hypercube gauche sera créé et affiché ainsi :
def hypercube( finesse = [3,3,3,3] ) pV222_0 = ... pV222_1 = ... pH2222 = PF.new( [ pV222_0, pV222_1 ] ) pH2222.build( finesse ) pH2222.draw( "P" ) end
Dans cet exemple, l'hypercube gauche est construit sur deux cubes gauches définis auparavant :
Nous découvrirons l'utilité d'une telle construction un peu plus loin, lorsque nous nous intéresserons au contenu des pFormes, et notamment à leurs pFormes diagonales et immergées.
Deux opérations, la construction d'une forme milieu et l'application récursive de cette construction permettent donc de produire une famille de formes multilinéaires. Nous noterons que la construction d'une forme milieu produit une forme de même dimension et que l'application récursive de cette construction produit une forme de dimension supérieure. Ces formes sont gauches dans le cas général (à l'exception du segment bien sûr), le quadrangle est une portion du classique paraboloïde hyperbolique, une surface réglée à double courbure négative, et les six faces du cube gauche sont des facettes gauches.
Ces formes sont 'pleines', chaque point d'une forme peut être adressé : un cube gauche est un volume rempli de points et non une simple enveloppe vide, et il en est de même pour l'hypercube. On a bien ainsi défini des formes cohérentes dans lesquelles il va être possible de travailler et dont on pourra par exemple extraire des 'sous–formes'. Une opération inverse, la diagonalisation, produisant une forme de dimension inférieure mais plus complexe qu'un segment linéaire nous conduira à de nouvelles extensions de ces opérations fondamentales.
Au sommaire de cette section :
Toutes les formes précédentes sont linéaires plusieurs fois, elles sont engendrées par des familles de droites, mais elles n'en contiennent pas moins de « vraies » courbes présentant un certain intérêt. La facette gauche, par exemple, est une surface en forme de selle de cheval présentant une certaine courbure dès que les deux segments générateurs ne sont plus coplanaires. En repliant une facette gauche sur elle–même jusqu'à confondre deux sommets opposés, on construit une sorte de triangle dont un côté est un arc de parabole. Ce 'pliage' de la facette gauche nous permet de construire une courbe, une forme de dimension inférieure à celle de la facette gauche, mais de complexité supérieure au segment de droite.
Soit une facette gauche construite sur deux segments (pL2_0,pL2_1) définis sur deux couples de points (p00,p01) et (p10,p11) :
pS22 = [ pL2_0, pL2_1 ] = [ [ p00, p01 ], [ p10, p11 ] ],
Le point milieu de la facette peut se définir par :
pm = ( (p00 + p01)/2 + (p10 +p11)/2 )/2 = ( p00 + p01 + p10 + p11 )/4
Ce point détermine quatre sous–facettes (p00,pm), (p01,pm), (p10,pm) et (p11,pm). En recommençant ainsi récursivement l'opération sur les deux sous–facettes en diagonale construites sur les couples (p00,pm) et (pm,p11), on engendre un ensemble infini et dense de points, une courbe reliant en diagonale les points p00 et p11 de la facette gauche. A ce stade rien ne nous permet de connaître les caractéristiques de cette courbe.
Cette forme est de dimension inférieure à celle de la facette et de degré supérieur à ses génératrices. On est amené à réécrire le point milieu en fonction de trois points :
pm = ( p0 + 2*p1 + p2 )/4 avec : p0 = p00, p1 = ( p01+p10 )/2, p2 = p11
Cette expression peut être retrouvée par un processus récursif en deux temps conduisant au point pm à partir du triplet (p0,p1,p2), à l'aide de deux points milieux intermédiaires q0 et q1:
1) q0 = (p0 + p1)/2, q1 = (p1 + p2)/2, 2) pm = (q0 + q1)/2
L'application récursive de ce processus pour une valeur infinie de la récursion produit un ensemble infini et "dense" de points situés "entre" les trois points p0, p1 et p2, un "segment de parabole" que nous désignerons par pL3 et que nous écrirons :
pL3 = [ p0,p1,p2 ]
En se souvenant de l'expression algébrique d'une facette gauche :
p(u,v) = (1-u)•(1-v)•p00 + u•(1-v)•p01 + (1-u)•v•p10 + u•v•p11, avec u,v dans l'intervalle [0,1] € R
on exprime la courbe diagonale en égalant les valeurs u et v et conduisant à :
p(u) = (1-u)2•p0 + 2•(1-u)•u•p1 + u2•p2 avec : p0 = p00, p1 = ( p01+p10 )/2, p2 = p11 et pour u = 1/2 p(1/2) = ( p0 + 2*p1 + p2 )/4
On retrouve bien l'expression du point milieu trouvée de façon géométrique. Une autre approche est également possible en écrivant :
p(t) = (1-t)•q0(t) + t•q1(t), q0(t) = (1-t)•p0 + t•p1, q1(t) = (1-t)•p1 + t•p2, soit : p(t) = (1-t)2•p0 + 2•(1-t)•t•p1 + t2•p2 avec t dans l'intervalle [0,1] € R
Nous retrouvons dans cette construction l'algorithme proposé par Paul de Faget de Casteljau pour le cas d'une parabole définie par trois points, un algorithme qui se trouve ainsi logiquement relié au processus de génération de la famille des formes multilinéaires récursives, par une sorte de contraction/pliage/diagonalisation d'une facette gauche. Quelle que soit l'approche on est en présence d'une courbe dite "de Bézier", du nom de son "inventeur" Pierre Bézier. Nous aurons l'occasion d'y revenir et d'éclairer les apports des uns et des autres dans ce domaine des formes courbes.
Dans la syntaxe SRpF, nous écrirons ainsi les deux approches :
1) à partir d'une facette gauche : def diag( ) pS22 = PF.new( tableau carré de points ) pS22.build( [3,3] ) # optionnel pS22.draw( "S" ) # optionnel diag = pS22.diag() diag.build( 0 ) # affichage du diag.draw( "L" ) # polygone contrôle diag.build( 3 ) diag.draw( "P" ) # end 2) à partir de trois points : def pL3( ) pL3 = PF.new( [ p0, p1, p2 ] ) pL3.build( 0 ) # affichage du pL3.draw( "L" ) # polygone contrôle pL3.build( 3 ) pL3.draw( "P" ) end
Noter que dans les deux cas, la courbe parabole est représentée sous la forme d'une série de points et accompagnée de son polygone de contrôle. On notera qu'elle passe par les points d'extrémité mais pas par le point intermédiaire.
Cette diagonalisation de la facette gauche nous a permis de construire une forme de dimension inférieure à celle de la facette gauche, mais de complexité supérieure au segment de droite. De façon plus générale le processus de diagonalisation va nous permettre de quitter le monde purement rectiligne des formes multilinéaires récursives et d'aborder celui de formes gauches plus complexes.
... à suivre !
def ship() coque = [ [[3.0/2,0,-1.0/32],[3.0/2,-1.0/4,-1.0/32],[3.0/2,-1.0/2,0],[3.0/2,-1.0/4,1.0/32],[3.0/2,0,1.0/32]], [[2.0/2,0,-1.0/32],[2.0/2,-1.0/4,-1.0/32],[2.0/2,-1.0,0],[2.0/2,-1.0/4,1.0/32],[2.0/2,0,1.0/32]], [[1.0/2,0,-1.0/16],[1.0/2,-1.0/2,-1.0/16],[1.0/2,-1.0,0],[1.0/2,-1.0/2,1.0/16],[1.0/2,0,1.0/16]], [[0.0/2,0,-1.0/2],[0.0/2,-1.0/2,-2.0/2],[0.0/2,-1.0,0],[0.0/2,-1.0/2,2.0/2],[0.0/2,0,1.0/2]], [[-1.0/2,0,-1.0/2],[-1.0/2,-1.0/2,-1.0/2],[-1.0/2,-1.0/4,0],[-1.0/2,-1.0/2,1.0/2],[-1.0/2,0,1.0/2]], [[-2.0/2,0,-1.0/4],[-2.0/2,-1.0/8,-1.0/4],[-3.0/2,-1.0/8,0],[-2.0/2,-1.0/8,1.0/4],[-2.0/2,0,1.0/4]] ] pCoque_ext = PF.new( coque ) # coque extérieure pCoque_ext.rotate( [90,0,0] ) # pivoté sur Ox pCoque_ext.draw( "L" ) # affichage des polygones de contrôle pCoque_in = pCoque_ext.parallel( -0.05 ) # coque intérieure ship = PF.new( [pCoque_ext.poles(), pCoque_in.poles()] ) # volume de la coque ship.build( [0,3,3] ) # construction des points du volume ship.draw( "V" ) # affichage en volume (les 6 faces) ship.draw( "L" ) # affichage en couples end