Class: Bestguigui::Render3D::ObjModel

Inherits:
Object
  • Object
show all
Includes:
OpenGL
Defined in:
lib/bestguigui/render_3d/obj_model.rb

Overview

Modèle 3D chargé depuis un fichier .obj. Export "Wavefront .obj" standard, ce que Blender et la plupart des logiciels 3D savent produire. Gère les objets/groupes en o ou g, avec ou sans normales, triangulés (une face = 3 sommets).

La texture est trouvée dans cet ordre :

1. Le .obj référence un .mtl (`mtllib`) → une texture par matériau
 (`usemtl`/`map_Kd`).
2. `texture:` est passé explicitement → toute la géométrie utilise
 cette texture (pratique quand plusieurs modèles partagent une
 même feuille de texture, ex: gfx/textures.png pour tout le jeu).
3. Sinon, une image du même nom que le .obj à côté de lui
 (car.obj -> car.png/.jpg) est cherchée et utilisée.
4. Si rien de tout ça ne donne de texture, erreur explicite plutôt
 qu'un crash cryptique au premier appel à #draw.

La géométrie est aplatie en un Mesh (VBO) par matériau (ou un seul Mesh si texture unique) au chargement — #draw ne fait plus que positionner/tourner et dessiner ces Mesh déjà en mémoire GPU, plutôt que de reconstruire des triangles en immediate mode à chaque frame.

desk = Bestguigui::Render3D::ObjModel.new("gfx/models/desk.obj", transparent: true)
desk.draw(camera, x, y, z, rotation_y)

car = Bestguigui::Render3D::ObjModel.new("gfx/car.obj", texture: shared_texture)
car.draw(camera, x, y, z, rotation_y, scale: 16) # .obj à l'échelle native de l'outil 3D

lighting: true bascule sur un éclairage Gouraud (Shader.gouraud, voir Bestguigui::Render3D::Light pour régler direction/couleur) — look "PS1/FF7", faces à l'ombre assombries plutôt qu'un rendu unlit plat. Utilise les normales du .obj (vn) si présentes ; sinon calcule une normale par face (aspect facetté, cohérent avec un rendu bas-poly).

hero = Bestguigui::Render3D::ObjModel.new("gfx/hero.obj", texture: tex, lighting: true)

Constant Summary collapse

IMAGE_EXTENSIONS =
%w[.png .jpg .jpeg].freeze

Instance Method Summary collapse

Constructor Details

#initialize(obj_path, transparent: false, texture: nil, lighting: false, shader: nil) ⇒ ObjModel

Returns a new instance of ObjModel.



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/bestguigui/render_3d/obj_model.rb', line 42

def initialize(obj_path, transparent: false, texture: nil, lighting: false, shader: nil)
  @transparent = transparent
  @lighting = lighting
  @shader = shader || (lighting ? Shader.gouraud : Shader.default)
  @groups = {} # material/groupe => Mesh (mode multi-matériaux)
  @mesh = nil  # mode texture unique

  vertices, tex_coords, normals = [], [], []
  faces = {}
  materials = nil
  current = "default"
  faces[current] = { material: nil, triangles: [] }

  File.readlines(obj_path).each do |line|
    infos = line.chomp.split(" ")

    case infos[0]
    when "mtllib"
      materials = MaterialCollection.new(File.join(File.dirname(obj_path), infos[1]))
    when "v"
      vertices.push infos.drop(1).map(&:to_f)
    when "vt"
      tex_coords.push infos.drop(1).map(&:to_f)
    when "vn"
      normals.push infos.drop(1).map(&:to_f)
    when "o", "g"
      if infos.size > 1
        current = infos.drop(1).join(" ")
        faces[current] ||= { material: nil, triangles: [] }
      end
    when "usemtl"
      faces[current][:material] = infos[1]
    when "f"
      # Chaque référence est "v/vt" ou "v/vt/vn" (parfois juste "v").
      triangle = infos[1..3].map { |v| v.split("/").map { |i| i.to_i - 1 } }
      faces[current][:triangles].push triangle
    end
  end

  @materials = materials
  @texture = texture || (materials.nil? ? find_sibling_texture(obj_path) : nil)

  if materials.nil? && @texture.nil?
    raise "Bestguigui::Render3D::ObjModel: no texture found for #{obj_path}" \
          "add a mtllib to the .obj, pass texture:, or put a same-named " \
          "image next to it (#{File.basename(obj_path, ".*")}.png)."
  end

  bake(faces, vertices, tex_coords, normals)
end

Instance Method Details

#draw(camera, x = 0, y = 0, z = 0, rotation_y = 0, scale: 1) ⇒ Object

scale: est utile quand le .obj a été exporté à l'échelle native de l'outil 3D (souvent des coordonnées ~0.01-1 dans Blender) plutôt qu'à l'échelle du monde du jeu — 1 par défaut, ne change rien si le modèle est déjà à la bonne taille.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/bestguigui/render_3d/obj_model.rb', line 97

def draw(camera, x = 0, y = 0, z = 0, rotation_y = 0, scale: 1)
  model = Mat4.translation(x, y, z) * Mat4.rotation_y(rotation_y) * Mat4.scaling(scale, scale, scale)

  @shader.use
  @shader.set_mat4("u_model", model)
  @shader.set_mat4("u_view", camera.view_matrix)
  @shader.set_mat4("u_projection", camera.projection_matrix)
  @shader.set_bool("u_alpha_test", @transparent)
  @shader.set_vec3("u_tint", 1, 1, 1)
  @shader.set_int("u_texture", 0)

  if @lighting
    @shader.set_vec3("u_light_dir", *Light.direction)
    @shader.set_vec3("u_light_color", *Light.color)
    @shader.set_float("u_ambient", Light.ambient)
  end

  if @texture
    @texture.bind
    @mesh.draw
  else
    @groups.each do |material, mesh|
      @materials.texture_for(material).bind
      mesh.draw
    end
  end
end