read in english

Les Bases d'OpenGL en OCaml

Premiers débuts avec OpenGL en OCaml

Voici une série d'exemples en partant du plus simple, pouvant être utiles (ou pas) lors de la découverte d'OpenGL en OCaml.

Attention: Avec OpenGL 3.X un système de dépréciation a été introduit. Voici texto l'extrait des spécifications qui introduit ce sujet:
"OpenGL 3.0 introduces a deprecation model in which certain features may be marked as deprecated. Deprecated features are expected to be completely removed from a future version of OpenGL."
Une traduction pourrait être:
OpenGL 3.0 indroduit un système de dépréciation qui a pour objet de marquer comme obsolète certaines fonctionnalités. Les fonctionnalités obsolètes sont susceptibles d'être totalement supprimées des futures versions d'OpenGL.
La majeur partie de ce didacticiel utilise des fonctionnalités obsolètes. Vous pouvez voir des exemples assurant la compatibilité descendante dans le répertoire "TEST3" de l'archive de glMLite.
Cependant veuillez noter que ce didacticiel reste tout de même intéressant dans le sens où la programmation OpenGL compatible 3.X correspond à un modèle plus complexe, aussi vous pouvez toujours débuter avec ce didacticiel, puis basculer vers le nouveau modèle ensuite.
Aussi si vous préférez apprendre l'OpenGL compatible descendant, vous pouvez utiliser cette autre documentation.


Introduction

L'OpenGL est un incontournable en programmation multimédia. Cette bibliothèque permet de réaliser de la 3D temps réel.
Les exemples présentés sur cette page utilisent l'interface openGL glMLite (GH). Vous pouvez visualiser la documentation de cette interface qui contient des liens vers les différentes pages de manuel des différentes fonctions de l'API openGL.
Les scripts suivants peuvent être exécuté avec la ligne de commande suivante :
 ocaml -I +glMLite  GL.cma Glut.cma demo.ml
Ou bien en ajoutant les instructions suivantes au tout début du fichier :
#! /usr/bin/env ocaml
#directory "+glMLite"
#load "GL.cma"
#load "Glut.cma"

Ou encore compilés pour des performances optimales :
 ocamlopt -I +glMLite  GL.cmxa Glut.cmxa demo.ml -o demo.exe

Voici une explication ligne par ligne, en commençant par le point d'entré du script.

Première chose, l'appel à glutInit avec en paramètre le tableau d'arguments du script permet d'initialiser la bibliothèque Glut.

Ensuite la fenêtre est crée avec glutCreateWindow , avec un titre qui apparaîtra dans son bandeau supérieur.

Ensuite la fonction de rappel (callback) d'affichage est enregistré (glutDisplayFunc ). À chaque fois que le rendu d'une frame sera requis, cette fonction sera appelée pour remplir cette office. (Nous allons décortiquer le contenu de la fonction display plus bas.)

Enfin glutMainLoop permet au programme d'entrer dans la boucle d'exécution principale (cette fonction de retourne jamais).

Voici maintenant une description de la fonction d'affichage.
La première fonction efface le contenu de l'image, sinon l'image est dessinée par dessus la frame précédente. Ce qui ne change pas grand chose ici sur une image fixe, mais vous pourrez voir la différence si vous enlevez cette instruction dans le script d'après.

Ensuite une couleur RGB est définie avec glColor3 par 3 flottants entre 0.0 et 1.0. Ici la couleur définie est le rouge, toutes les primitives qui suivront seront dessinées avec cette couleur jusqu'au prochain appel à cette fonction. Si aucune couleur n'est définie, la couleur par défaut est le blanc.

Ensuite la fonction glBegin annonce que l'on va définir un triangle. Entre cette fonction et glEnd qui indique la fin de la primitive, il devra donc y avoir trois points définis avec glVertex2 .

open GL
open Glut

let display() =
  glClear [GL_COLOR_BUFFER_BIT];
  glColor3 ~r:1.0 ~g:0. ~b:0.;
  glBegin GL_TRIANGLES;
    glVertex2 (-1.0) (-1.0);
    glVertex2 ( 0.0) ( 1.0);
    glVertex2 ( 1.0) (-1.0);
  glEnd();
;;

let () =
  ignore(glutInit Sys.argv);
  glutInitDisplayMode [GLUT_SINGLE];
  ignore(glutCreateWindow ~title:"simple demo");
  glutDisplayFunc ~display;
  glutMainLoop();
;;
Voici l'affichage produit par ce script :

Première Animation

Voici un autre script dérivé du précédent. Il a quelques modifications pour faire une animation rudimentaire.

Pour que l'animation soit fluide, le mode d'affichage sélectionné avec glutInitDisplayMode est un affichage en double buffer, ce qui signifie qu'il y a deux images pour le rendu, une image en arrière plan est utilisée pour dessiner les primitives demandées, et lorsque ce rendu est finit il est échangé avec l'image affichée à l'écran, à l'aide de la fonction glutSwapBuffers placée à la fin de la fonction d'affichage display.

Une fonction de rappel en Idle est aussi définie, il s'agit d'une fonction qui est appelée lorsque l'ordinateur n'a plus rien à faire. Ici cette fonction déclenchera un nouvel affichage avec glutPostRedisplay .

Une rotation est défine avec glRotate basée sur la fonction Sys.time.

Une autre différence est la définition d'une nouvelle couleur avant chaque point, ce qui provoquera une facette avec trois couleurs, une à chacun de ces somments, vous pouvez voir l'effet produit sur l'image après le script :

open GL
open Glut

let display() =
  glClear [GL_COLOR_BUFFER_BIT];
  glRotate ~angle:(Sys.time() *. 0.2) ~x:0.0 ~y:0.0 ~z:1.0;
  glBegin GL_TRIANGLES;
  glColor3 ~r:1.0 ~g:0.0 ~b:0.0;  glVertex2 (-1.0) (-1.0);
  glColor3 ~r:0.0 ~g:1.0 ~b:0.0;  glVertex2 ( 0.0) ( 1.0);
  glColor3 ~r:0.0 ~g:0.0 ~b:1.0;  glVertex2 ( 1.0) (-1.0);
  glEnd();
  glutSwapBuffers();
;;

let () =
  ignore(glutInit Sys.argv);
  glutInitDisplayMode [GLUT_DOUBLE];
  ignore(glutCreateWindow ~title:"simple demo");
  glutDisplayFunc ~display;
  glutIdleFunc ~idle:(glutPostRedisplay);
  glutMainLoop();
;;

Les callbacks d'interaction

Voici les fonctions de rappels permettant de récupérer les interactions avec le clavier et la souris :

open GL
open Glut

(* mouse coordinates *)
let xold = ref 0
let yold = ref 0

let b_down = ref false

let angley = ref 0
let anglex = ref 0

let display() =
  glClear [GL_COLOR_BUFFER_BIT];
  glLoadIdentity ();
  glRotate ~angle:(float(- !angley)) ~x:1.0 ~y:0.0 ~z:0.0;
  glRotate ~angle:(float(- !anglex)) ~x:0.0 ~y:1.0 ~z:0.0;
  glColor3 ~r:0. ~g:1.0 ~b:0.;
  glutWireCube ~size:1.0;
  glFlush();
  glutSwapBuffers();
;;

(* active mouse motion *)
let motion ~x ~y =
  if !b_down then  (* if the left button is down *)
  begin
 (* change the rotation angles according to the last position
    of the mouse and the new one *)
    anglex := !anglex + (!xold - x);
    angley := !angley + (!yold - y);
    glutPostRedisplay ();
  end;
  xold := x;  (* save mouse position *)
  yold := y;
;;

(* mouse button event *)
let mouse ~button ~state ~x ~y =
  match button, state with
  (* if we press the left button *)
  | GLUT_LEFT_BUTTON, GLUT_DOWN ->
      b_down := true;
      xold := x;  (* save mouse position *)
      yold := y;
  (* if we release the left button *)
  | GLUT_LEFT_BUTTON, GLUT_UP ->
      b_down := false;
  | _ -> ()
;;

let keyboard ~key ~x ~y =
  match key with
  | '\027' (* escape key *)
  | 'q' -> exit 0
  | _ -> ()
;;

let () =
  ignore(glutInit Sys.argv);
  glutInitDisplayMode [GLUT_DOUBLE];
  ignore(glutCreateWindow ~title:Sys.argv.(0));
  glutDisplayFunc ~display;
  glutKeyboardFunc ~keyboard;
  glutMouseFunc ~mouse;
  glutMotionFunc ~motion;
  glutMainLoop();
;;

(utilisez la souris pour faire tourner le cube)


La bibliothèque GLU

La bibliothèque GLU fournit des fonctions courantes utiles, mais n'apporte pas vraiement de fonctionnalité OpenGL en plus, car en fait toutes les fonctions GLU sont implémentées avec des fonctions OpenGL.
ocamldoc > Glu

#load "Glu.cma"
open Glu

Redimentionnement de la fenêtre

Il est très utile également de fournir une fonction de rappel pour gérer le redimentionnement éventuel de la fenêtre, car si les dimentions de la fenêtre changent il faut alors ajuster la matrice de projection des objets à la nouvelle taille de la fenêtre.

let reshape ~width:w ~height:h =

  (* set viewport to be the entire window *)
  glViewport 0 0 w h;

  (* aspect ratio of the window *)
  let aspect = (float w) /. (float h) in

  (* load the projection matrix to be modified *)
  glMatrixMode GL_PROJECTION;

  (* reinitialise the projection matrix (to the identity matrix) *)
  glLoadIdentity ();

  (* set the projection matrix,
     in other words: define the perspective view *)
  gluPerspective ~fovy:60.0 ~aspect ~zNear:0.5 ~zFar:80.0;

  (* switch to modelview matrix in order to set scene *)
  glMatrixMode GL_MODELVIEW;
;;

L'argument fovy est très simple, il s'agit de l'angle de vue, f.o.v. pour "field of view" en anglais, et le "y" parceque c'est l'angle entre la gauche et la droite c'est à dire suivant l'axe Y qui est l'axe vertical.
Des valeurs élevées pour fovy augmenteront l'impression de perspective, mais des valeurs trop élevées ne seront plus réaliste, par exemple un angle de vue à 180 degrés.
Des valeurs classiques courantes sont entre 45 et 60.

Les arguments zNear et zFar définissent l'intervale dans lequel les objets seront affichés le long de l'axe Z, c'est à dire la profondeur dans l'image ou l'axe du regard dans l'image en son centre.
Il pourrait alors être tentant lorsque l'on débute de mettre respectivement une valeur très petite et une très grande, mais ce serait une erreur car alors ça réduirait la précision de l'affichage des objets au niveau de leur différence d'éloignement et en particulier la précision de l'affichage des intersections entre les objets proches ainsi que ceux qui s'intercroisent.
Plus précisément c'est lorsque deux facettes se coupent mutuellement ou lorsque l'une affleure le centre de l'autre que la précision de l'affichage de l'intersection entre les deux sera imprécise.
Plus l'écart entre zNear et zFar est petit et plus la précision de l'affichage de la profondeur sera précise. C'est pourquoi il est important de définir zFar en fonction de l'éloignement maximal que peut avoir un objet dans la représentation.

La fonction de rappel reshape sera appelée une première fois avant l'affichage de la première frame.

  glutReshapeFunc ~reshape;

Dé-projection

Voici un exemple qui montre comment transformer les coordonnées de la souris qui sont récupérées en coordonnées 2D au niveau de l'espace de la fenêtre, pour les transformer en coordonnées 3D utilisable dans la représentation 3D :

open GL
open Glu
open Glut

(* position of the mouse pointer *)
let px = ref 0.0
let py = ref 0.0

(* try to force refresh every "min_refresh" milliseconds *)
let min_refresh = 30

(* coordinate bounds of the window *)
let x_min = -6.0 and x_max = 6.0
and y_min = -6.0 and y_max = 6.0
and z_min, z_max = -6.0, 60.0 ;;

(* left button down *)
let ldown = ref false


let display() =
  glClear [GL_COLOR_BUFFER_BIT];
  glLoadIdentity ();

  glPointSize 5.0;
  glBegin GL_POINTS;

  if !ldown
  then glColor3 1.0 0.0 0.0
  else glColor3 1.0 1.0 0.0;

  (* just display the mouse pointer *)
  glVertex2 !px !py;

  glEnd ();

  glFlush ();
  glutSwapBuffers ();
;;


(* convert the coordinates of the mouse
   from window coordinates to the local
   representation *)
let reg_unproject_coords ~x ~y =
  let mx, my, _ = gluUnProjectUtil ~x ~y in
  px := mx;
  py := my;
;;

(* active mouse motion *)
let motion ~x ~y =
  reg_unproject_coords ~x ~y;
;;

(* passive mouse motion *)
let passive ~x ~y =
  reg_unproject_coords ~x ~y;
;;

(* mouse button event *)
let mouse ~button ~state ~x ~y =
  reg_unproject_coords ~x ~y;
  match button, state with
  | GLUT_LEFT_BUTTON, GLUT_DOWN -> ldown := true;
  | GLUT_LEFT_BUTTON, GLUT_UP -> ldown := false;
  | _ -> ()
;;

let keyboard ~key ~x ~y =
  match key with
  | 'q' | '\027' -> exit 0
  | _ -> ()
;;

let reshape  ~width:w ~height:h =
  glViewport 0  0  w h;
  glMatrixMode GL_PROJECTION;
  glLoadIdentity ();
  if w <= h then
    glOrtho x_min x_max (y_min *. float h /. float w)
                        (y_max *. float h /. float w) z_min z_max
  else
    glOrtho (x_min *. float w /. float h)
            (x_max *. float w /. float h) y_min y_max z_min z_max;

  glMatrixMode GL_MODELVIEW;
  glLoadIdentity ();
;;

let idle () =
  glutPostRedisplay ();
;;

let gl_init () =
  glClearColor 0.5 0.5 0.5  0.0;
  glShadeModel GL_FLAT;
;;

let () =
  ignore(glutInit Sys.argv);
  glutInitDisplayMode [GLUT_DOUBLE; GLUT_RGB];
  glutInitWindowSize 600 600;
  glutInitWindowPosition 100 100;
  ignore(glutCreateWindow Sys.argv.(0));
  glutSetCursor GLUT_CURSOR_NONE;

  gl_init ();

  glutDisplayFunc ~display;
  glutReshapeFunc ~reshape;
  glutIdleFunc ~idle;
  glutMouseFunc ~mouse;
  glutKeyboardFunc ~keyboard;
  glutMotionFunc ~motion;
  glutPassiveMotionFunc ~passive;

  glutMainLoop ();
;;

Aller plus loin

Voilà c'était très court pour une première approche mais l'OpenGL n'est pas si compliqué car c'est un langage purement descriptif. Dans l'archive de glMLite vous trouverez plein de petits exemples intéressants dans le répertoire RedBook-Samples. Ce sont les exemples tirés de l'ouvrage de référence le red-book qui ont été converti en OCaml.


GNU/FDL   © 2007 2008   Florent Monnier
 

Notice d'utilisation

Ce document est distribué sous licence FDL, ce qui signifie que vous pouvez le redistribuer avec ou sans améliorations du moment d'en conserver le droit d'auteur (copyright) et la licence.
Vous êtes également invité à m'écrire pour me suggérer des améliorations, ou simplement pour me soumettre des suggestions.
 

Vous pouvez aussi utiliser la licence CC-by-sa (n'importe quelle version).

Camellement vôtre !
The OCaml Language