BLENDER TRIBU
Vous souhaitez réagir à ce message ? Créez un compte en quelques clics ou connectez-vous pour continuer.
-29%
Le deal à ne pas rater :
DYSON V8 Origin – Aspirateur balai sans fil
269.99 € 379.99 €
Voir le deal

Aller en bas
avatar
Navi

Résolu comment engendrer un mouvement selon un autre dans un simple système

Lun 18 Sep - 13:23
comment engendrer un mouvement selon un autre dans un simple système Capture
comment engendrer un mouvement selon un autre dans un simple système Capture-RIG

bonjour,

je débute sur blender et notamment les armatures , je sais comment connecter mes bones aux objets , faire des rotations, translations mais là où j'ai du mal c'est lorsqu' un mouvement doit engendrer un autre mouvement, alors je me suis fait un exemple , avec 2 chariots : un chariot X et un chariot Z qui coulisse sur leurs rails respectifs . les 2 sont lié par un tube de liaison si bien que dans la logique , si je tire chariot x , chariot z devrait descendre et inversement .
alors j'ai appliqué des contraintes mais :
- lorsque je tire chariot x qui contient le bone parent la contrainte est respecté : le chariot ne se déplace que en x suivant les limites que j'ai fixé MAIS il emmène tout le système avec lui sans se préoccuper des contraintes appliqué à chariot z .
- lorsque je fait une rotation du tube de liaison, chariot z suit le mouvement malgré les contraintes appliqués à celui-ci
- lorsque je tire chariot z , il est complètement bloqué.

Je pense que dans la logique de blender , le bone parent ne se préoccupe pas des contraintes appliqué aux bones enfants ce qui me pose beaucoup de soucis .

merci de m'avoir lu jusque là, j'espère avoir été compréhensible dans une certaine concision. je ne sais pas si vous pourriez éclaircir ma lanterne.
bonne journée à vous Smile
Blender Moonboots
Blender Moonboots

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Lun 18 Sep - 14:16
Je ne sais pas comment tu t'y es pris jusque là (ce serait intéressant que tu décrives les contraintes et parentages créés) mais le problème que j'entrevois (si j'ai bien compris ton problème bien sûr) c'est que la contrainte peut marcher dans un sens mais pas dans l'autre, car Blender n'accepte pas les dépendances circulaires (un objet A peut contrôler un objet B mais l'objet B peut aussi contrôler l'objet A). En général tu peux quand même te débrouiller pour créer l'illusion que ça fonctionne. Dans ton cas, faut-il vraiment que ça fonctionne dans les 2 sens ?

_________________
Ma chaîne de tutos sur Youtube
avatar
CBY

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Mar 19 Sep - 9:39
Bonjour,
Peut être un moyen simple en jouant simplement sur la position de 3 objets:
. la glissière en X en translation en X
. la glissière en Z en translation en Z
. le tirant en rotation et position

à t=1  définir un point clef
. en location pour les 2 glissières
. en location et rotation pour le tirant

à t=100  définir un point clef
 positionner la glissière en Z à z=0 (location)
 positionner le tirant à z=0 en position horizontale (location+ rotation)
 positionner  la glissière en X à la position cohérente avec le tirant  (location)

On remarquera qu'il n'y a aucune contrainte entre les objets mais que l'illusion de mouvement entre t=1 et t=100 semble cohérente.
Par contre il n'est pas sûr que mécaniquement ce soit rigoureux pour les puristes mécaniciens dont je suis. Tout est dans l'illusion!

https://www.dropbox.com/scl/fi/dj5ipo3fobwwg5no4wr2y/glissi-res.blend?rlkey=q7b5agqivu3v2o0707hwhhh2o&dl=0
avatar
Navi

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Mar 19 Sep - 11:27
tout d'abord merci pour vos réponses , ça fait plaisir d'avoir des façons de penser différentes . car étant débutant , je me suis dit si j'applique une contrainte sur le chariot X pour qu'il ne se déplace que en X et de la même façon une contrainte sur le chariot Z , le tout étant relié ba .. Blender n'a plus qu'à ce débrouiller mais ça ne fonctionne pas.
alors j'ai trouvé une solution à peu près viable.
j'ai créé 2 armatures , une pour chariot X et une pour chariot Z avec la liaison.
ensuite j'ai appliqué la contrainte transformation pour que chariot Z copie la position de chariot X depuis la position X sur l'axe Z . en gros la coordonnée X de chariot X est mappé sur la coordonnée Z de chariot Z . donc plus chariot X avance , plus chariot Z monte .
ensuite j'avais plus qu'a mettre un locked track sur le tube de liaison pour qu'il se dirige toujours vers chariot X .
l'illusion est presque parfaite !
je dit presque car avec cette méthode chariot Z évolue linéairement par rapport à chariot X . si chariot X avance d'un mètre -> chariot Y monte d'un mètre .
or je me souviens de mes cours de 4ème lorsque j'était encore assidue:
dans un triangle rectangle , les rapports de longueur ce définisse avec l'équation H² = CA² + CO²
on connais H² qui est mon tube de liaison  donc CA = racine ( H² -  CO² )
en remplaçant les termes, si je place mon système à l'origine de mon repère on a : chariot Z = racine ( liaison ² - chariot X ² )  avec liaison ² , une constante .
ce qui prouve que ce système n'évolue pas linéairement comme pourrait le supposer la première réflexion .
je crois que je vais maintenant me penché sur python , pour faire un truc qui fonctionne .
merci à tous
avatar
CBY

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Mar 19 Sep - 12:34
Bonjour,
Les déplacements ne sont pas linéaires, ils varient en fonction de l'angle de la barre de liaison.
Angle de la barre avec l'horizontale: alpha
Longueur de la barre L
abscisse du chariot en X : L*cos(alpha)
ordonnée du chariot en Z : L*sin(alpha)

Blender ce n'est pas CATIA.

Bon courage!

avatar
CBY

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Mar 19 Sep - 15:20
Bonjour,
La version mathématique avec des drivers. Le déplacement du cube rouge en x fait tourner la barre et glisser le cube bleu en Z.
Faire un clic droit/edit driver sur les zone violette dans "transform" pour voir les formules.

Nota: à la création, pour initialiser les valeurs mettre la barre à 45°, l'abscisse de X et l'ordonnée de Z sont égales, calculer (Pythagore ) la longueur de la barre (hypoténuse).

Ne déplacer que le cube rouge !!! Ce n'est pas réversible, les relations étant hiérarchiques.

https://www.dropbox.com/scl/fi/92onwlsus0n6ob5i9xnqj/glissi-res_01.blend1?rlkey=6c7v9lx6hh98qiruvnwp2fua8&dl=0

C'est presque CATIA!

Navi aime ce message

avatar
Navi

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Mar 19 Sep - 19:00
merci CBY , votre exemple est étonnant d'efficacité . les drivers ça à l'aire pas mal du tout dans mon cas .
alors j'ai pu appliqué les driver dans mon exemple par contre , on est d' accord que cylinder fait sa rotation mais il ce déplace aussi en X suivant la position de cube X. mais en location X je vois juste une valeur -4 , pas de driver , pas de bones . d'où vient donc la magie du déplacement de cylinder?
sur le miens j'ai due mettre un driver sur X location de cylinder avec la valeur X de CubeX .

en tout cas merci j'ai appris un truc aujourd'hui .

avatar
CBY

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Mer 20 Sep - 10:18
Bonjour
Je suis en déplacement, donc de mémoire :
.le cylindre est parenté (sans l'option keep transform) au cube en x. Sélection du cylindre puis du cube X, CTRL P.
Voir la hiérarchie des objets.
.son origine a été déplacée de son centre de gravité à son extrémité inférieure qui coïncide avec celle du cube en x
avatar
Navi

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Mer 20 Sep - 10:36
Bonjour,
merci beaucoup pour vos explications CBY , j'avance à grand pas !
dernière question , après j'arrête , peut on faire une limite circulaire ? je veux dire il y a t-il moyen que la limite en X ou en Y de cube X soit la longueur du cylindre . ça sort de l'exemple , mais ça me sera très utile .
avatar
CBY

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Mer 20 Sep - 12:09
Bonjour
Je pense l'avoir fait dans l'exemple, voir les limites définies en x et z pour les cubes.


PS: je suis en déplacement, pas de PC disponible.
3D Topic
3D Topic

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Jeu 21 Sep - 16:05
bonjour, je vous propose une autre méthode, les contraintes, faire glissez le cube rouge sur l'axe z.
A votre disposition si vous avez des questions....très sympa le drivers !
https://www.swisstransfer.com/d/9f8bb7e0-6fe1-41b1-b708-77f9399c10a8
avatar
CBY

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Jeu 21 Sep - 17:11
Bonjour,
Le cylindre rentre dans cube001 et ne reste pas sur le CDG du cube001 (ou le bord de celui-ci). La liaison (cylindre) n'est pas de longueur constante entre les 2 cubes lors du mouvement.
Mais il y a de l'idée pour faire un coulisseau.

comment engendrer un mouvement selon un autre dans un simple système Glissi10
3D Topic
3D Topic

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Jeu 21 Sep - 17:48
oops je n'avais pas compris que le cylindre devait être fixe...
avatar
Navi

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Jeu 21 Sep - 18:50
comment engendrer un mouvement selon un autre dans un simple système Captur13

bonjour,

j'ai fait un exemple un peu plus poussé, même principe, mais plutôt que de verrouiller A seulement sur un axe , il peut se déplacer en tous points du plan X, Y avec Z=0 et X<350 et Y<350 .

la coordonnée z de B est calculé  et fait √ ( 5002 - OA2) avec OA = √ ( x2 + y2 ) . x et y les coordonné de A. pas très utile pour l'animation mais c'est pour la vérification car j'ai un peu galéré je l'avoue.
pour la rotation X de AB ,  on a asin ( OA / 500 ) .
après avoir cherché pendant très très longtemps comment mixer la rotation en X et en Y avec toutes les formules trigonométriques que j'ai pu trouver, développer etc... ( three day later ...) la solution la plus simple était de faire une petite rotation en Z après la rotation en X .
donc calcule de l'angle entre l'axe des X et OA.
acos ( Ox / OA )
acos ( x / √ ( x2 + y2 ) )
ça ne marche pas, c'est décalé donc on fait un quart de tour :
-pi/2 + acos ( x / √ ( x2 + y2 ) )
ensuite ça ne marche pas pour les y négatifs donc on fait une condition :
-pi/2 + acos ( x / √ ( x2 + y2 ) ) if y > 0 else -pi/2 - acos ( x / √ ( x2 + y2 ) )

voilà, c'est nickel maintenant , juste j'aurais aimé une limite circulaire , plutôt que de dire -350 < X < 350 et -350 < Y < 350 avec Z = 0 . ce qui n'ai pas circulaire mais carré ..

en tout cas merci pour votre aide , c'est le début d'un grand projet Wink

le résultat:
https://www.dropbox.com/scl/fi/ac9lkjx20rkw3anfhd63p/exemple.blend?rlkey=zkf4q7l26b9yia1zw6rc18z29&dl=0

Edit : j'ai trouver la contrainte Limit Distance , elle était sous mes yeux
avatar
CBY

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Ven 22 Sep - 10:23
Bonjour Navi,
Content que tu sois arrivé à la solution avec un peu de trigonométrie. Quel est le projet qui incorpore ce mécanisme?

Juste une remarque: l'objet B (sphere 002) est redondant avec sphere 001 et peut être supprimé.
Sphere 001 étant parenté à AB sa position n'a pas besoin d'être calculée, elle correspond au point B.
avatar
Navi

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Ven 22 Sep - 19:14
comment engendrer un mouvement selon un autre dans un simple système Captur14

Effectivement, B n'a pas besoin d'être calculé mais c'était pour vérifier que les rotations se fassent bien et puis ça me donne une information en plus qui me sera utile par la suite.
Le projet serait de modéliser une imprimante 3d delta , en optimisant la longueur des "Bras" distance (A B) par rapport à la surface d'impression voulu qui est elle même liée au volume de l'imprimante. Car plus les bras sont court, plus les angles deviennent important, la précision décroit et les petits moteurs pas à pas doivent tourner plus vite pour déplacer la tête d'impression sur une même distance . Trop long , il y a plus de poids, d'inertie , et la hauteur d'impression est grandement réduite  . Tout ça, c'est une histoire de compromis . Pour l'instant ce n'est qu'un premier jet mais je pense pouvoir faire quelque chose d'un peu plus représentatif de la réalité . Aussi peut être faire un script par la suite pour que ça soit paramétrable .
L'objectif serait de la modéliser, tel que je l'imagine et de la construire , sans prétention commerciale , juste pour le fun .

https://www.dropbox.com/scl/fi/dbz4lcg3uyjk6n2s0syj8/exemple3Dprinter.blend?rlkey=shm58okpoz4rgdercynz8bsic&dl=0
avatar
CBY

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Sam 23 Sep - 10:53
Bonjour Navi,

Bonne modélisation de l'imprimante 3D, je vois que tu avances rapidement dans Blender.
Le projet est ambitieux, et il faudra surement faire de l'usinage.
Personnellement j'ai opté pour la facilité avec une Creality ENDER 3 pro.
Pour la création de pièces mécaniques j'utilise Freecad. Freecad a un module non officiel "Animation" que je n'ai pas encore utilisé.
avatar
Navi

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Lun 25 Sep - 14:46
salut à tous,

CBY, j'ai un modèle similaire mais plus ancien , une tarantula qui commence a ce faire vieille mais qui imprime toujours aussi bien avec un peu de maintenance . d'ailleurs je pourrais imprimer quelques pièces pour ce projet. sinon je pense prendre des pièces de modèle existant plutôt que de me lancer dans l'usinage .

j'aime beaucoup blender, il y a longtemps, je passais beaucoup de temps a modéliser des personnages mais je n'avais jamais fait d'animation . Il y a déjà tellement de chose à apprendre sur ce logiciel que je n'ai pas l'envie de passer à un autre, je pense qu'il fera l'affaire .

pour revenir au sujet , j'ai fait un petit script pour automatisé toute la création des drivers. on dit que A , B et AB, 3 objets.

On peut placer A et B où l'on veut . si il sont plus distant que la longueur de AB , il ni aura pas de bug , AB restera sur A et pointera vers B . on ne peut pas déplacer B sur l'axe Z car la coordonnée Z de B est calculer .

seul impératif : l'objet AB doit être extrudé en Z, car le script prendra la longueur AB pour ses calculs . la position de AB et son origine , n'ont pas d'importance, le script se débrouille.

si on change la longueur de AB , on relance le script et les drivers sont mis a jour avec la nouvelle longueur . parce que oui c'est chiant de tout modifier à la main , ou bien je suis un grand fainéant  comment engendrer un mouvement selon un autre dans un simple système 1f636
à part ça , il fonctionne pour n'importe quels objets , suffit d'en avoir 3 et de connaitre leurs noms et on écrit SimpleRotationAB('A', 'AB', 'B' ) avec 'A' 'AB' et 'B' le nom de vos objets bien sure  .  Je n'avais pas d'idée pour le nom de la fonction, il y a que ça qui cloche  quoi? si vous avez des idées  tres souriant

édit : ajout d'offsets pour les puristes . je vais pouvoir dessiné de belles rotules sans que AB les traverses jusqu'au centre du point de pivot  tres souriant

Code:
import bpy

class DriverVariable :

    fromObject = ""                    # le nom de l objet pour créé la variable a mettre dans le driver
    name = ""                          # nom de la variable de notre driver
    type = 'TRANSFORMS'                # par défaut de type transforms
    transform_type = 'LOC_X'            # il a pour valeur la coordonnée x de fromObject
    transform_space = 'WORLD_SPACE'    # WORLD_SPACE par défaut sinon LOCAL_SPACE
    driver = 0
   
    def createVariable (self, name) :              # methode qui passe tout les parametres a blender pour qu il créé la variable de notre driver
        self.name = name
        d = self.driver.driver.variables.new()
        d.name = name
        d.type = self.type
        d.targets[0].id = bpy.data.objects.get(self.fromObject)
        d.targets[0].transform_type = self.transform_type
        d.targets[0].transform_space = self.transform_space
   
    def run (self, expression) :        # on entre le calcul a realiser par le driver
        self.driver.driver.expression = expression



class DriverVariableXYZ () :
   
    x = DriverVariable()    # on créé une instance DriverVariable pour chaque axes ,
    y = DriverVariable()    # il ne sont pas actif jusqu à l appel de la methode createVariable , qui va nous créé la variable dans le driver.
    z = DriverVariable()    # l appel de createVariable sera à la charge de l utilisateur
    driver = 0
    toObject = ""
   
    def __init__ (self, fromObject , toObject) : 
   
        self.toObject = toObject
   
        self.x.transform_type = 'LOC_X'    # on veut la coordonnée X
        self.x.fromObject = fromObject      # de l objet fromObject passée en paramètre
       
        self.y.transform_type = 'LOC_Y'
        self.y.fromObject = fromObject
       
        self.z.transform_type = 'LOC_Z'
        self.z.fromObject = fromObject
       
   
    def createDriver(self, driver, arg) :
       
        self.driver = bpy.data.objects[ self.toObject ].driver_add( driver, arg)
        self.x.driver = self.driver
        self.y.driver = self.driver
        self.z.driver = self.driver
        return self.driver
           
       
    def setDriver(self,driver) :
       
        self.driver = driver
        self.x.driver = driver
        self.y.driver = driver
        self.z.driver = driver
       
       
       
    def run (self, expression) :
        self.driver.driver.expression = expression



def removeDriver(objectName) :            # fonction permettant de supprimer les drivers déjà present sur l objet
   
    object = bpy.data.objects[ objectName ]
    od = object.animation_data.drivers.values()
    for d in od:
        object.driver_remove(d.data_path)


def setOriginToZextremity(object_Name, offset) : # fonction pour mettre l origine d un objet a son extremité sur l axe Z ( sur la moitier de sa longeur en partant de son centre )
     
      object = bpy.data.objects[ object_Name ]
      objectlength = object.dimensions[2]
     
      object.rotation_euler = (0, 0, 0)                                    # on le remet a sa rotation d origine
     
      bpy.ops.object.select_all(action='DESELECT')                        # on désélectionne tout les objets
      object.select_set(True)                                              # pour sélectionner que notre objet   
      bpy.context.view_layer.objects.active = object
     
      bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')  # on met l origine au centre de l objet
      object.location = (0.0, 0.0, objectlength/2 + offset)                # on le centre a l origine de notre repère avec une translation de la moitié de sa longueur
      saved_location = bpy.context.scene.cursor.location.copy()
      bpy.context.scene.cursor.location = (0.0, 0.0, 0.0)                  # on met le curseur a l origine du repère
      bpy.ops.object.origin_set(type='ORIGIN_CURSOR')                      # on met l origine de l objet sur le curseur
      bpy.context.scene.cursor.location = saved_location                  # on remet le curseur là où il était
      object.select_set(False)                                            # on désélectionne notre objet





def SimpleRotationAB(A_Name, AB_Name, B_Name, *offset) :  # avec A n importe ou dans l espace et B fixe en x y
                                                            # offset facultatif , jusqu à 2
       
       
        removeDriver( AB_Name)                    # on supprime les drivers present sur nos objet pour en cree d autre
        removeDriver( B_Name)
       
        # -------- on calcul la longueur AB et on positionne son origine en prenant en compte l offset ------------------


       
        AB_length = bpy.data.objects[ AB_Name ].dimensions[2]                      # on prend la longueur de AB
       
        if len( offset ) == 0 :                            # pas de offset definie, on met l origine de AB a son extremite sans offset
           
            setOriginToZextremity( AB_Name, 0.0 )
           
        elif len( offset ) == 1 :                          # si il y a qu un offset, on partage l offset sur chaques extremités
           
            setOriginToZextremity( AB_Name, offset[0]/2 )
            AB_length += offset[0]
           
        else :                                            # si il y a 2 offset, on applique le premier a l extremité A et le 2ème a l extrémité B
           
            setOriginToZextremity( AB_Name, offset[0] )
            AB_length += offset[0] + offset[1]
           

       
        # ------------------------- driver sur la rotation en X de AB par rapport aux coordonnees de A ------------------
       

       
        A = DriverVariableXYZ( A_Name , AB_Name)
        driver = A.createDriver('rotation_euler',0)
        A.fromObject = A_Name

        A.x.createVariable('Ax')
        A.y.createVariable('Ay')
       
        B = DriverVariableXYZ( B_Name, AB_Name )
        B.setDriver( driver )
        B.x.createVariable('Bx')
        B.y.createVariable('By')
       
       
        A.run( 'asin( sqrt( ( Ax - Bx )**2 + ( Ay - By )**2 ) / %d ) if sqrt( ( Ax - Bx )**2 + ( Ay - By )**2 ) < %d else pi/2' % (AB_length, AB_length) )
       
        # -------------------------- driver sur la rotation en Z de AB avec les coordonnees de A --------------------------
       
        A = DriverVariableXYZ( A_Name , AB_Name )
        driver = A.createDriver('rotation_euler',2)
        A.fromObject = A_Name
        A.x.createVariable('Ax')
        A.y.createVariable('Ay')
       
        B = DriverVariableXYZ( B_Name, AB_Name )    # on a 2 variables de 2 objets differents pour un driver sur l objet AB ( rotation en Z )
        B.setDriver( driver )                      # on reprend donc le driver cree avec A , pour injecter nos 2 autres variables Bx et By
        B.x.createVariable('Bx')
        B.y.createVariable('By')
       
        A.run( '-pi/2 + copysign( 1 , Ay-By ) * acos(  ( Ax - Bx ) / sqrt( ( Ax - Bx )**2 + ( Ay - By )**2 ) ) ')
       
        # -------------------------- driver sur la position xy de AB avec les coordonnees de A ---------------------------
       
        A = DriverVariableXYZ( A_Name , AB_Name )
        A.fromObject = A_Name
       
        A.createDriver('location',0)
        A.x.createVariable('Ax')
        A.run( 'Ax')
       
        A.createDriver('location',1)
        A.y.createVariable('Ay')
        A.run( 'Ay')
       
        A.createDriver('location',2)
        A.z.createVariable('Az')
        A.run( 'Az')
       
        # -------------------------  driver sur la position Z de B avec les coordonnees de A -----------------------------
       
        A = DriverVariableXYZ( A_Name , B_Name )
        driver = A.createDriver('location',2)
        A.fromObject = A_Name

        A.x.createVariable('Ax')
        A.y.createVariable('Ay')
        A.z.createVariable('Az')
       
        B = DriverVariableXYZ( B_Name, AB_Name )
        B.setDriver( driver )
        B.x.createVariable('Bx')
        B.y.createVariable('By')
       
        A.run( 'sqrt( %d - sqrt( ( Ax - Bx )**2 + ( Ay - By )**2 )**2 ) + Az if sqrt( ( Ax - Bx )**2 + ( Ay - By )**2 ) < %d else Az'
        % ( AB_length * AB_length , AB_length )  )   
 
 
        # ---------------------------------------      the end    ---------------------------------------






SimpleRotationAB('A', 'AB', 'B' ) # pas de offset
#SimpleRotationAB('A', 'AB', 'B', 100 ) # 1 offset de 50 sur chaque extrémité
#SimpleRotationAB('A', 'AB', 'B', 100 , 0 ) # un offset de 100 côté A et pas de offset côté B
#SimpleRotationAB('A', 'AB', 'B', 100, 130) # un offset de 100 côté A et de 130 côté B
Contenu sponsorisé

Résolu Re: comment engendrer un mouvement selon un autre dans un simple système

Revenir en haut
Permission de ce forum:
Vous ne pouvez pas répondre aux sujets dans ce forum