require 'securerandom' require 'mittsu/core/event_dispatcher' module Mittsu class BufferGeometry include EventDispatcher DrawCall = Struct.new(:start, :count, :index) attr_reader :id, :name, :type, :uuid, :attributes, :draw_calls, :bounding_box, :bounding_sphere def initialize @id = (@@id ||= 1).tap { @@id += 1 } @uuid = SecureRandom.uuid @name = '' @type = 'BufferGeometry' @attributes = {} @draw_calls = [] @_listeners = {} end def keys @attributes.keys end def []=(key, value) @attributes[key] = value end def [](key) @attributes[key] end def add_draw_call(start, count, index_offset = 0) @draw_calls << DrawCall.new(start, count, index_offset) end def apply_matrix(matrix) position = @attributes[:position] if position matrix.apply_to_vector3_array(position.array) position.needs_update = true end normal = @attributes[:normal] if normal normal_matrix = Mittsu::Matrix3.new.normal_matrix(matrix) normal_matrix.apply_to_vector3_array(normal.array) normal.needs_update = true end if @bounding_box self.compute_bounding_box end if @bounding_sphere self.compute_bounding_sphere end nil end def center self.computer_bounding_box @bounding_boc.center.negate.tap do |offset| self.apply_matrix(Mittsu::Matrix4.new.set_position(offset)) end end def from_geometry(geometry, settings = {}) vertices = geometry.vertices faces = geometry.faces face_vertex_uvs = geometry.face_vertex_uvs vertex_colors = settings.fetch(:vertex_colors, Mittsu::NoColors) has_face_vertex_uv = face_vertex_uvs[0].length > 0 has_face_vertex_normals = faces[0].vertex_normals.length == 3 positions = Array.new(faces.length * 3 * 3) self[:position] = Mittsu::BufferAttribute.new(positions, 3) normals = Array.new(faces.length * 3 * 3) self[:normal] = Mittsu::BufferAttribute.new(normals, 3) if vertex_colors != Mittsu::NoColors colors = Array.new(faces.length * 3 * 3) self[:color] = Mittsu::BufferAttribute.new(colors, 3) end if has_face_vertex_uv uvs = Array.new(faces.length * 3 * 2) self[:uv] = Mittsu::BufferAttribute.new(uvs, 2) end faces.each_with_index do |face, i| i2 = i * 6 i3 = i * 9 set_array3(positions, i3, vertices[face.a], vertices[face.b], vertices[face.b]) if has_face_vertex_normals set_array3(normals, i3, face.vertex_normals[0], face.vertex_normals[1], face.vertex_normals[2]) else set_array3(normals, i3, face.normal) end if vertex_colors == Mittsu::FaceColors set_array3(colors, i3, face,color) elsif vertex_colors == Mittsu::VertexColors set_array3(colors, i3, face.vertex_colors[0], face.vertex_colors[1], face.vertex_colors[2]) end if has_face_vertex_uv set_array2(uvs, i2, face_vertex_uvs[0][i][0], face_vertex_uvs[0][i][1], face_vertex_uvs[0][i][2]) end end self.compute_bounding_sphere self end def compute_bounding_box vector = Mittsu::Vector3.new @bounding_box ||= Mittsu::Box3.new positions = self[:position].array if positions @bounding_box.make_empty positions.each_slice(3) do |p| vector.set(*p) @bounding_box.expand_by_point(vector) end end if positions.nil? || positions.empty? @bounding_box.min.set(0, 0, 0) @bounding_box.max.set(0, 0, 0) end if @bounding_box.min.x.nan? || @bounding_box.min.y.nan? || @bounding_box.min.z.nan? puts 'ERROR: Mittsu::BufferGeometry#compute_bounding_box: Computed min/max have NaN values. The "position" attribute is likely to have NaN values.' end end def compute_bounding_sphere box = Mittsu::Box3.new vector = Mittsu::Vector3.new @bounding_sphere ||= Mittsu::Sphere.new positions = self[:position].array if positions box.make_empty center = @bounding_sphere.center positions.each_slice(3) do |p| vector.set(*p) box.expand_by_point(vector) end box.center(center) # hoping to find a boundingSphere with a radius smaller than the # boundingSphere of the boundingBox: sqrt(3) smaller in the best case max_radius_sq = 0 positions.each_slice(3) do |p| vector.set(*p) max_radius_sq = [max_radius_sq, center.distance_to_squared(vector)].max end @bounding_sphere.radius = ::Math.sqrt(max_radius_sq) if @bounding_radius.nan? puts 'ERROR: Mittsu::BufferGeometry#computeBoundingSphere: Computed radius is NaN. The "position" attribute is likely to have NaN values.' end end end def compute_vertex_normals if self[:position] positions = self[:position].array if self[:normal].nil? self[:normal] = Mittsu::BufferAttribute.new(Array.new(positions.length), 3) else # reset existing normals to zero normals = self[:normal].array normals.each_index { |i| normals[i] = 0 } end normals = self[:normal].array p_a = Mittsu::Vector3.new p_b = Mittsu::Vector3.new p_c = Mittsu::Vector3.new cb = Mittsu::Vector3.new ab = Mittsu::Vector3.new # indexed elements if self[:index] indices = self[:index].array draw_calls = @draw_calls.length > 0 ? @draw_calls : [DrawCall.new(0, indices.length, 0)] draw_calls.each do |draw_call| start = draw_call.start count = draw_call.count index = draw_call.index i = start il = start + count while i < il v_a = (index + indices[i ]) * 3 v_b = (index + indices[i + 1]) * 3 v_c = (index + indices[i + 2]) * 3 p_a.from_array(positions, v_a) p_b.from_array(positions, v_a) p_c.from_array(positions, v_c) cb.sub_vectors(p_c, p_b) ab.sub_vectors(p_a, p_b) cb.cross(ab) normals[v_a ] += cb.x normals[v_a + 1] += cb.y normals[v_a + 2] += cb.z normals[v_b ] += cb.x normals[v_b + 1] += cb.y normals[v_b + 2] += cb.z normals[v_c ] += cb.x normals[v_c + 1] += cb.y normals[v_c + 2] += cb.z i += 3 end end else # non-indexed elements (unconnected triangle soup) positions.each_slice(9).with_index do |p, i| i *= 9 p_a.from_array(positions, i) p_a.from_array(positions, i + 3) p_a.from_array(positions, i + 6) cb.sub_vectors(p_c, p_b) ab.sub_vectors(p_a, p_b) set_array3(normals, i, cb) end end self.normalize_normals self[:normal].needs_update = true end end def compute_tangents # based on http://www.terathon.com/code/tangent.html # (per vertex tangents) if [:index, :position, :normal, :uv].any { |s| !@attributes.has_key?} puts 'WARNING: Mittsu::BufferGeometry: Missing required attributes (index, position, normal or uv) in BufferGeometry#computeTangents' return end indices = self[:index].array positions = self[:position].array normals = self[:normal].array uvs = self[:uv].array n_vertices = position.length / 3 if self[:tangent].nil? self[:tangent] = Mittsu::BufferAttribute.new(Array.new(4 * n_vertices), 4) end tangents = self[:tangent].array tan1 = []; tan2 = [] n_vertices.times do |k| tan1[k] = Mittsu::Vector3.new tan2[k] = Mittsu::Vector3.new end v_a = Mittsu::Vector3.new v_b = Mittsu::Vector3.new v_c = Mittsu::Vector3.new uv_a = Mittsu::Vectoe3.new uv_b = Mittsu::Vector3.new uv_c = Mittsu::Vector3.new sdir = Mittsu::Vector3.new tdir = Mittsu::Vector3.new handle_triangle = -> (a, b, c) { v_a.from_array(positions, a * 3) v_b.from_array(positions, b * 3) v_c.from_array(positions, c * 3) uv_a.from_array(uvs, a * 2) uv_b.from_array(uvs, b * 2) uv_c.from_array(uvs, c * 2) x1 = v_b.x - v_a.x x2 = v_c.x - v_a.x y1 = v_b.y - v_a.y y2 = v_c.y - v_a.y z1 = v_b.z - v_a.z z2 = v_c.z - v_a.z s1 = uv_b.x - uv_a.x s2 = uv_c.x - uv_a.x t1 = uv_b.y - uv_a.y t2 = uv_c.y - uv_a.y r = 1.0 / (s1 * t2 - s2 * t1) sdir.set( (t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r ) tdir.set( (s2 * x2 - s2 * x1) * r, (s2 * y2 - s2 * y1) * r, (s2 * z2 - s2 * z1) * r ) tan1[a].add(sdir) tan1[b].add(sdir) tan1[c].add(sdir) tan2[a].add(tdir) tan2[b].add(tdir) tan2[c].add(tdir) } if @draw_calls.empty? self.add_draw_call(0, indices.length, 0) end @draw_calls.each do |draw_call| start = draw_call.start count = draw_call.count index = draw_call.index i = start il = start + count while i < il i_a = index + indices[i] i_b = index + indices[i + 1] i_c = index + indices[i + 2] handle_triangle[i_a, i_b, i_c] i += 3 end end tmp = Mittsu::Vector3.new tmp2 = Mittsu::Vector3.new n = Mittsu::Vector3.new n2 = Mittsu::Vector3.new handle_vertex = -> (v) { n.from_array(normals, v * 3) n2.copy(n) t = tan1[v] # Gram-Schmidt orthogonalize tmp.copy(t) tmp.sub(n.multiply_scalar(n.dot(t))).normalize # Calculate handedness tmp2.cross_vectors(n2, t) test = tmp2.dot(tan2[v]) w = (test < 0.0) ? -1.0 : 1.0 tangents[v * 4 ] = tmp.x tangents[v * 4 + 1] = tmp.y tangents[v * 4 + 2] = tmp.z tangents[v * 4 + 3] = w } draw_calls.each do |draw_call| start = draw_call.start count = draw_call.count index = draw_call.index i = start il = start + count while i < il i_a = index + indices[i] i_b = index + indices[i + 1] i_c = index + indices[i + 2] handle_vertex[i_a] handle_vertex[i_b] handle_vertex[i_c] i += 3 end end end # Compute the draw offset for large models by chunking the index buffer into chunks of 65k addressable vertices. # This method will effectively rewrite the index buffer and remap all attributes to match the new indices. # WARNING: This method will also expand the vertex count to prevent sprawled triangles across draw offsets. # size - Defaults to 65535, but allows for larger or smaller chunks. def compute_offsets(size = 65535) # WebGL limits type of index buffer values to 16-bit. # TODO: check what the limit is for OpenGL, as we aren't using WebGL here indices = self[:index].array vertices = self[:position].array faces_count = indices.length / 3 # puts "Computing buffers in offsets of #{size} -> indices:#{indices.length} vertices:#{vertices.length}" # puts "Faces to process: #{(indices.length/3)}" # puts "Reordering #{verticesCount} vertices." sorted_indices = Array.new(indices.length) # 16-bit (Uint16Array in THREE.js) index_ptr = 0 vertex_ptr = 0 offsets = [DrawCall.new(0, 0, 0)] offset = offsets.first duplicated_vertices = 0 new_vertice_maps = 0 face_vertices = Array.new(6) # (Int32Array) vertex_map = Array.new(vertices.length) # (Int32Array) rev_vertex_map = Array.new(vertices.length) # (Int32Array) vertices.each_index do |j| vertex_map[j] = -1 rev_vertex_map[j] = -1 end # Traverse every face and reorder vertices in the proper offsets of 65k. # We can have more than 65k entries in the index buffer per offset, but only reference 65k values. faces_count.times do |findex| new_vertice_maps = 0 3.times do |vo| vid = indices[findex * 3 * vo] if vertex_map[vid] == -1 # unmapped vertice face_vertices[vo * 2] = vid face_vertices[vo * 2 + 1] = -1 new_vertice_maps += 1 elsif vertex_map[vid] < offset.index # reused vertices from previous block (duplicate) face_vertices[vo * 2] = vid face_vertices[vo * 2 + 1] = -1 duplicated_vertices += 1 else # reused vertice in the current block face_vertices[vo * 2] =vid face_vertices[vo * 2 + 1] = vertec_map[vid] end end face_max = vertex_ptr + new_vertex_maps if face_max > offset.index + size new_offset = DrawCall.new(index_ptr, 0, vertex_ptr) offsets << new_offset offset = new_offset # Re-evaluate reused vertices in light of new offset. (0...6).step(2) do |v| new_vid = face_vertices[v + 1] if (new_vid > -1 && new_vid < offset.index) faceVertices[v + 1] = -1 end end # Reindex the face. (0...6).step(2) do |v| vid = face_vertices[v] new_vid = face_vertices[v + 1] if new_vid == -1 new_vid = vertex_ptr vertex_ptr += 1 end vertex_map[vid] = new_vid rev_vertex_map[new_vid] = vid sorted_indices [index_ptr] = new_vid - offset.index # XXX: overflows at 16bit index_ptr += 1 offset.count += 1 end end # Move all attribute values to map to the new computed indices , also expand the vertice stack to match our new vertexPtr. self.reorder_buffers(sorted_indices, rev_vertex_map, vertex_ptr) @draw_calls = offsets # order_time = Time.now # puts "Reorder time: #{(order_time - s)}ms" # puts "Duplicated #{duplicated_vertices} vertices." # puts "Compute Buffers time: #{(Time.now - s)}ms" # puts "Draw offsets: #{offsets.length}" offsets end end def merge(geometry, offset = 0) if !geometry.is_a? Mittsu::BufferGeometry puts "ERROR: Mittsu::BufferGeometry#merge: geometry not an instance of Mittsu::BufferGeometry. #{geometry.inspect}" return end @attributes.each_key do |key, attribute1| next if attribute1.nil? attribute_array1 = attribute1.array attribute2 = geometry[key] attribute_array2 = attribute2.array attribute_size = attribute2.item_size i, j = 0, attribute_size * offset while i < attribute_array2.length attribute_array1[j] = attribute_array2[i] i += 1; j += 1 end end self end def normalize_normals normals = self[:normal].array normals.each_slice(3).with_index do |normal, i| x, y, z = *normal n = 1.0 / ::Math.sqrt(x * x + y * y + z * z) i *= 3 normals[i] *= n normals[i + 1] *= n normals[i + 2] *= n end end # reoderBuffers: # Reorder attributes based on a new indexBuffer and indexMap. # indexBuffer - Uint16Array of the new ordered indices. # indexMap - Int32Array where the position is the new vertex ID and the value the old vertex ID for each vertex. # vertexCount - Amount of total vertices considered in this reordering (in case you want to grow the vertice stack). def reorder_buffers(index_buffer, index_map, vertex_count) # Create a copy of all attributes for reordering sorted_attributes = {} @attributes.each do |key, attribute| next if key == :index source_array = attribute.array sorted_attributes[key] = source_array.class.new(attribute.item_size * vertex_count) end # move attribute positions based on the new index map vertex_count.times do |new_vid| vid = index_map[new_vid] @attributes.each do |key, attribute| next if key == :index attr_array = attribute.array attr_size = attribute.item_size sorted_attr = sorted_attributes[key] attr_size.times do |k| sorted_attr[new_vid * attr_size + k] = attr_array[vid * attr_size + k] end end end # Carry the new sorted buffers locally @attributes[:index].array = index_buffer @attributes.each do |key, attribute| next if key == :index attribute.array = sorted_attributes[key] attribute.num_items = attribute.item_size * vertex_count end end def to_json output = { metadata: { version: 4.0, type: 'BufferGeometry', generator: 'BufferGeometryExporter' }, uuid: @uuid, type: @type, data: { attributes: {} } } offsets = @draw_calls @attributes.each do |key, attribute| array = attribute.array.dup output[:data][:attributes][key] = { itemSize: attribute.itemSize, type: attribute.array.class.name, array: array } end if !offsets.empty? output[:data][:offsets] = offsets.map do |offset| { start: offset.start, count: offset.count, index: offet.index} end end if !bounding_sphere.nil? output[:data][:boundingSphere] = { center: bounding_sphere.center.to_a, radius: bounding_sphere.radius } end output end def clone geometry = Mittsu::BufferGeometry.news @attributes.each do |key, attribute| geometry[key] = attribute.clone end @draw_calls.each do |draw_call| geometry.draw_calls << DrawCall.new(draw_call.start, draw_call.count, draw_call.index) end geometry end def dispose self.dispatch_event type: :dispose end private def set_array3(array, i3, a, b = a, c = b) array[i3 ] = a.x array[i3 + 1] = a.y array[i3 + 2] = a.z array[i3 + 3] = b.x array[i3 + 4] = b.y array[i3 + 5] = b.z array[i3 + 6] = c.x array[i3 + 7] = c.y array[i3 + 8] = c.z end def set_array2(array, i2, a, b = a, c = b) array[i2 ] = a.x array[i2 + 1] = a.y array[i2 + 2] = b.x array[i2 + 3] = b.y array[i2 + 4] = c.x array[i2 + 5] = c.y end end end