lib/cosmos/gui/opengl/gl_viewer.rb in cosmos-3.5.1 vs lib/cosmos/gui/opengl/gl_viewer.rb in cosmos-3.5.2

- old
+ new

@@ -1,724 +1,724 @@ -# encoding: ascii-8bit - -# Copyright 2014 Ball Aerospace & Technologies Corp. -# All Rights Reserved. -# -# This program is free software; you can modify and/or redistribute it -# under the terms of the GNU General Public License -# as published by the Free Software Foundation; version 3 with -# attribution addendums as found in the LICENSE.txt - -# This file is inspired by the FOX Gui toolkit's FXGLViewer class - -require 'cosmos' -require 'cosmos/gui/qt' -require 'cosmos/gui/opengl/opengl' - -module Cosmos - - class GlViewer < Qt::GLWidget - MAX_PICKBUF = 1024 - MAX_SELPATH = 64 - EPS = 1.0e-2 - PICK_TOL = 3 - DTOR = 0.0174532925199432957692369077 - RTOD = 57.295779513082320876798154814 - - attr_accessor :projection # :PARALLEL or :PERSPECTIVE - attr_reader :zoom - attr_reader :fov - attr_reader :wvt - attr_reader :diameter - attr_reader :distance - attr_reader :orientation - attr_reader :center - attr_reader :scale - attr_reader :transform - attr_reader :itransform - attr_reader :maxhits - attr_reader :top_background - attr_reader :bottom_background - attr_reader :ambient - attr_reader :light - attr_reader :material - attr_reader :dropped - attr_reader :selection - attr_reader :scene - attr_reader :smode - attr_reader :options - - attr_accessor :selection_callback - attr_accessor :draw_axis - def initialize(parent) - super(parent) - - @defaultCursor = nil - @dragCursor = nil - @projection = :PERSPECTIVE - @zoom = 1.0 - @fov = 30.0 - @wvt = GlViewport.new - @diameter = 2.0; - @distance = 7.464116; - @orientation = Quaternion.new([0.0, 0.0, 0.0, 1.0]) - @center = [0.0, 0.0, 0.0] - @scale = [1.0, 1.0, 1.0] - updateProjection() - updateTransform() - @maxhits = 512; - @top_background = [0.5, 0.5, 1.0, 1.0] - @bottom_background = [1.0, 1.0, 1.0, 1.0] - @ambient = [0.2, 0.2, 0.2, 1.0] - @light = GlLight.new - @material = GlMaterial.new - @dial = [0, 0, 0] - @dropped = nil - @selection = nil - @scene = nil - @mode = :HOVERING - @draw_axis = nil - @options = [] - - @selection_callback = nil - end - - def minimumSizeHint - return Qt::Size.new(100, 100) - end - - def sizeHint - return Qt::Size.new(400, 400) - end - - def scene=(scene) - @scene = scene - @scale = [1.0, 1.0, 1.0] - if @scene - self.bounds = @scene.bounds - @zoom = @scene.zoom - @orientation = @scene.orientation - @center = @scene.center - @projection = @scene.projection - end - updateProjection() - updateTransform() - updateGL() - end - - def fov=(fov) - fov = 2.0 if fov < 2.0 - fov = 90.0 if fov > 90.0 - @fov = fov - tn = tan(0.5 * DTOR * @fov) - @distance = @diameter / tn - updateProjection() - updateTransform() - updateGL() - end - - def distance=(distance) - distance = @diameter if distance < @diameter - distance = 114.0 * @diameter if distance > (114.0 * @diameter) - if distance != @distance - @distance = distance - @fov = 2.0 * RTOD * atan2(@diameter, @distance) - updateProjection() - updateTransform() - updateGL() - end - end - - def zoom=(zoom) - zoom = 1.0e-30 if zoom < 1.0e-30 - if zoom != @zoom - @zoom = zoom - updateProjection() - updateGL() - end - end - - def scale= (scale) - scale[0] = 0.000001 if scale[0] < 0.000001 - scale[1] = 0.000001 if scale[1] < 0.000001 - scale[2] = 0.000001 if scale[2] < 0.000001 - if scale != @scale - @scale = scale - updateTransform() - updateGL() - end - end - - def orientation= (orientation) - if (orientation.q0 != @orientation.q0) or (orientation.q1 != @orientation.q1) or (orientation.q2 != @orientation.q2) or (orientation.q3 != @orientation.q3) - @orientation = orientation.clone.normalize - updateTransform() - update() - end - end - - def bounds= (bounds) - # Model center - @center = bounds.center - - # Model size - @diameter = bounds.longest - - # Fix zero size model - @diameter = 1.0 if @diameter < 1.0e-30 - - # Set equal scaling initially - @scale = [1.0, 1.0, 1.0] - - # Reset distance (and thus field of view) - self.distance = 1.1 * @diameter - end - - def center= (center) - if center != @center - @center = center - updateTransform() - updateGL() - end - end - - def selection= (shape) - @selection = shape - @selection_callback.call(shape) if @selection_callback - updateGL() - end - - def translate(vector) - @center[0] += vector[0] - @center[1] += vector[1] - @center[2] += vector[2] - updateTransform() - updateGL() - end - - def selectHits(x, y, w, h) - mh = @maxhits - nhits = 0 - makeCurrent() - - # Where to pick - pickx = (@wvt.w - 2.0*x - w) / w.to_f - picky = (2.0*y + h - @wvt.h) / h.to_f - pickw = @wvt.w / w.to_f - pickh = @wvt.h / h.to_f - - # Set pick projection matrix - GL.MatrixMode(GL::PROJECTION); - GL.LoadIdentity() - GL.Translatef(pickx, picky, 0.0) - GL.Scalef(pickw, pickh, 1.0) - case projection - when :PARALLEL - GL.Ortho(@wvt.left, @wvt.right, @wvt.bottom, @wvt.top, @wvt.hither, @wvt.yon) - when :PERSPECTIVE - GL.Frustum(@wvt.left, @wvt.right, @wvt.bottom, @wvt.top, @wvt.hither, @wvt.yon) - end - - # Model matrix - GL.MatrixMode(GL::MODELVIEW); - GL.LoadMatrixf(@transform) - - # Loop until room enough to fit - while true - nhits = 0 - buffer = GL.SelectBuffer(mh) - GL.RenderMode(GL::SELECT); - GL.InitNames() - GL.PushName(0) - @scene.hit(self) if @scene - GL.PopName() - nhits = GL.RenderMode(GL::RENDER) - mh <<= 1 - break if nhits >= 0 - end - doneCurrent() - return buffer.unpack("L*"), nhits - end - - def processHits(pickbuffer, nhits) - if nhits > 0 - zmin = 4294967295 - zmax = 4294967295 - i = 0 - while nhits > 0 - n = pickbuffer[i] - d1 = pickbuffer[1+i] - d2 = pickbuffer[2+i] - if ((d1 < zmin) || ((d1 == zmin) && (d2<=zmax))) - zmin = d1 - zmax = d2 - sel = i - end - i += n + 3 - nhits -= 1 - end - return @scene.identify(pickbuffer[4 + sel]) - end - return nil - end - - def pick(x, y) - obj = nil - if @scene and @maxhits - pickbuffer, nhits = selectHits(x-PICK_TOL, y-PICK_TOL, PICK_TOL*2, PICK_TOL*2) - obj = processHits(pickbuffer, nhits) if nhits > 0 - end - return obj; - end - - def initializeGL - GL.GetError() - - # Initialize GL context - GL.RenderMode(GL::RENDER) - - # Fast hints - GL.Hint(GL::POLYGON_SMOOTH_HINT, GL::FASTEST) - GL.Hint(GL::PERSPECTIVE_CORRECTION_HINT, GL::FASTEST) - GL.Hint(GL::FOG_HINT, GL::FASTEST) - GL.Hint(GL::LINE_SMOOTH_HINT, GL::FASTEST) - GL.Hint(GL::POINT_SMOOTH_HINT, GL::FASTEST) - - # Z-buffer test on - GL.Enable(GL::DEPTH_TEST) - GL.DepthFunc(GL::LESS) - GL.DepthRange(0.0, 1.0) - GL.ClearDepth(1.0) - GL.ClearColor(@top_background[0], @top_background[1], @top_background[2], @top_background[3]) - - # No face culling - GL.Disable(GL::CULL_FACE) - GL.CullFace(GL::BACK) - GL.FrontFace(GL::CCW) - - # Two sided lighting - GL.LightModeli(GL::LIGHT_MODEL_TWO_SIDE, 1) - GL.LightModel(GL::LIGHT_MODEL_AMBIENT, @ambient) - - # Preferred blend over background - GL.BlendFunc(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA) - - # Light on - GL.Enable(GL::LIGHT0) - GL.Light(GL::LIGHT0, GL::AMBIENT, @light.ambient) - GL.Light(GL::LIGHT0, GL::DIFFUSE, @light.diffuse) - GL.Light(GL::LIGHT0, GL::SPECULAR, @light.specular) - GL.Light(GL::LIGHT0, GL::POSITION, @light.position) - GL.Light(GL::LIGHT0, GL::SPOT_DIRECTION, @light.direction) - GL.Lightf(GL::LIGHT0, GL::SPOT_EXPONENT, @light.exponent) - GL.Lightf(GL::LIGHT0, GL::SPOT_CUTOFF, @light.cutoff) - GL.Lightf(GL::LIGHT0, GL::CONSTANT_ATTENUATION, @light.c_attn) - GL.Lightf(GL::LIGHT0, GL::LINEAR_ATTENUATION, @light.l_attn) - GL.Lightf(GL::LIGHT0, GL::QUADRATIC_ATTENUATION, @light.q_attn) - - # Viewer is close - GL.LightModeli(GL::LIGHT_MODEL_LOCAL_VIEWER, 1) - - # Material colors - GL.Material(GL::FRONT_AND_BACK, GL::AMBIENT, @material.ambient) - GL.Material(GL::FRONT_AND_BACK, GL::DIFFUSE, @material.diffuse) - GL.Material(GL::FRONT_AND_BACK, GL::SPECULAR, @material.specular) - GL.Material(GL::FRONT_AND_BACK, GL::EMISSION, @material.emission) - GL.Materialf(GL::FRONT_AND_BACK, GL::SHININESS, @material.shininess) - - # Vertex colors change both diffuse and ambient - GL.ColorMaterial(GL::FRONT_AND_BACK, GL::AMBIENT_AND_DIFFUSE) - GL.Disable(GL::COLOR_MATERIAL) - - # Simplest and fastest drawing is default - GL.ShadeModel(GL::FLAT) - GL.Disable(GL::BLEND) - GL.Disable(GL::LINE_SMOOTH) - GL.Disable(GL::POINT_SMOOTH) - GL.Disable(GL::COLOR_MATERIAL) - - # Lighting - GL.Disable(GL::LIGHTING) - - # No normalization of normals (it's broken on some machines anyway) - GL.Disable(GL::NORMALIZE) - - # Dithering if needed - GL.Disable(GL::DITHER) - end - - def paintGL - # Set viewport - GL.Viewport(0, 0, @wvt.w, @wvt.h) - - # Reset important stuff - GL.ShadeModel(GL::SMOOTH) - GL.PolygonMode(GL::FRONT_AND_BACK, GL::FILL) - GL.Disable(GL::LIGHTING) - GL.Disable(GL::ALPHA_TEST) - GL.Disable(GL::BLEND) - GL.Disable(GL::DITHER) - GL.Disable(GL::FOG) - GL.Disable(GL::LOGIC_OP) - GL.Disable(GL::POLYGON_SMOOTH) - GL.Disable(GL::POLYGON_STIPPLE) - GL.Disable(GL::STENCIL_TEST) - GL.Disable(GL::CULL_FACE) - GL.Disable(GL::COLOR_MATERIAL) - - # Reset matrices - GL.MatrixMode(GL::PROJECTION) - GL.LoadIdentity - GL.MatrixMode(GL::MODELVIEW) - GL.LoadIdentity - - # Clear to solid background - GL.ClearDepth(1.0) - GL.ClearColor(@top_background[0], @top_background[1], @top_background[2], @top_background[3]) - if @top_background == @bottom_background - begin - GL.Clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT) - rescue - # Raises false error on Mac - end - else # Clear to gradient background - begin - GL.Clear(GL::DEPTH_BUFFER_BIT) - rescue - # Raises false error on Mac - end - GL.Disable(GL::DEPTH_TEST) - GL.DepthMask(GL::FALSE) - GL.Begin(GL::TRIANGLE_STRIP) - GL.Color(@bottom_background); GL.Vertex3f(-1.0, -1.0, 0.0); GL.Vertex3f(1.0, -1.0, 0.0) - GL.Color(@top_background); GL.Vertex3f(-1.0, 1.0, 0.0); GL.Vertex3f(1.0, 1.0, 0.0) - begin - GL.End - rescue - # Raises false error on Mac - end - end - - # Depth test on by default - GL.DepthMask(GL::TRUE) - GL.Enable(GL::DEPTH_TEST) - - # Switch to projection matrix - GL.MatrixMode(GL::PROJECTION) - GL.LoadIdentity - case @projection - when :PARALLEL - GL.Ortho(@wvt.left, @wvt.right, @wvt.bottom, @wvt.top, @wvt.hither, @wvt.yon) - when :PERSPECTIVE - GL.Frustum(@wvt.left, @wvt.right, @wvt.bottom, @wvt.top, @wvt.hither, @wvt.yon) - end - - # Switch to model matrix - GL.MatrixMode(GL::MODELVIEW) - GL.LoadIdentity - - # Set light parameters - GL.Enable(GL::LIGHT0) - GL.Light(GL::LIGHT0, GL::AMBIENT, @light.ambient) - GL.Light(GL::LIGHT0, GL::DIFFUSE, @light.diffuse) - GL.Light(GL::LIGHT0, GL::SPECULAR, @light.specular) - GL.Light(GL::LIGHT0, GL::POSITION, @light.position) - GL.Light(GL::LIGHT0, GL::SPOT_DIRECTION, @light.direction) - GL.Lightf(GL::LIGHT0, GL::SPOT_EXPONENT, @light.exponent) - GL.Lightf(GL::LIGHT0, GL::SPOT_CUTOFF, @light.cutoff) - GL.Lightf(GL::LIGHT0, GL::CONSTANT_ATTENUATION, @light.c_attn) - GL.Lightf(GL::LIGHT0, GL::LINEAR_ATTENUATION, @light.l_attn) - GL.Lightf(GL::LIGHT0, GL::QUADRATIC_ATTENUATION, @light.q_attn) - - # Default material colors - GL.Material(GL::FRONT_AND_BACK, GL::AMBIENT, @material.ambient) - GL.Material(GL::FRONT_AND_BACK, GL::DIFFUSE, @material.diffuse) - GL.Material(GL::FRONT_AND_BACK, GL::SPECULAR, @material.specular) - GL.Material(GL::FRONT_AND_BACK, GL::EMISSION, @material.emission) - GL.Materialf(GL::FRONT_AND_BACK, GL::SHININESS, @material.shininess) - - # Color commands change both - GL.ColorMaterial(GL::FRONT_AND_BACK, GL::AMBIENT_AND_DIFFUSE) - - # Global ambient light - GL.LightModel(GL::LIGHT_MODEL_AMBIENT, @ambient) - - # Enable fog - if @options.include?(:VIEWER_FOG) - GL.Enable(GL::FOG) - GL.Fog(GL::FOG_COLOR, @top_background) # Disappear into the background - GL.Fogf(GL::FOG_START, (@distance - @diameter).to_f) # Range tight around model position - GL.Fogf(GL::FOG_END, (@distance + @diameter).to_f) # Far place same as clip plane:- clipped stuff is in the mist! - GL.Fogi(GL::FOG_MODE, GL::LINEAR) # Simple linear depth cueing - end - - # Dithering - GL.Enable(GL::DITHER) if @options.include?(:VIEWER_DITHER) - - # Enable lighting - GL.Enable(GL::LIGHTING) if @options.include?(:VIEWER_LIGHTING) - - # Set model matrix - GL.LoadMatrixf(@transform) - - if (@draw_axis and @draw_axis > 0) - # Draw axis - GL.PushMatrix - GL.LineWidth(2.5) - GL.Color3f(1.0, 0.0, 0.0) - GL.Begin(GL::LINES) - GL.Vertex3f(-@draw_axis.to_f, 0.0, 0.0) - GL.Vertex3f(@draw_axis.to_f, 0.0, 0.0) - begin - GL.End - rescue - # Raises false error on Mac - end - GL.Color3f(0.0, 1.0, 0.0) - GL.Begin(GL::LINES) - GL.Vertex3f(0, -@draw_axis, 0.0) - GL.Vertex3f(0, @draw_axis, 0) - begin - GL.End - rescue - # Raises false error on Mac - end - GL.Color3f(0.0, 0.0, 1.0) - GL.Begin(GL::LINES) - GL.Vertex3f(0, 0, -@draw_axis) - GL.Vertex3f(0, 0, @draw_axis) - begin - GL.End - rescue - # Raises false error on Mac - end - GL.PopMatrix - end - - # Draw what's visible - @scene.draw(self) if @scene - end - - def resizeGL(width, height) - @wvt.w = width; - @wvt.h = height; - updateProjection() - end - - def screenToEye(sx, sy, eyez) - e = [0.0, 0.0, 0.0] - xp = (@worldpx*sx + @ax).to_f - yp = (@ay - @worldpx*sy).to_f - if @projection == :PERSPECTIVE - if @distance != 0.0 - e.x = [((-eyez*xp) / @distance).to_f, ((-eyez*yp) / @distance).to_f, eyez] - end - else - e = [xp, yp, eyez] - end - return e; - end - - def screenToTarget(sx, sy) - [@worldpx*sx.to_f + @ax, @ay - @worldpx*sy.to_f, -@distance.to_f] - end - - def eyeToWorld(e) - [e[0]*@itransform[0][0] + e[1]*@itransform[1][0] + e[2]*@itransform[2][0] + @itransform[3][0], - e[0]*@itransform[0][1] + e[1]*@itransform[1][1] + e[2]*@itransform[2][1] + @itransform[3][1], - e[0]*@itransform[0][2] + e[1]*@itransform[1][2] + e[2]*@itransform[2][2] + @itransform[3][2]] - end - - def worldToEyeZ(w) - w[0]*@transform[0][2] + w[1]*@transform[1][2] + w[2]*@transform[2][2] + @transform[3][2] - end - - def calc_prime(v) - return [v[0]*@itransform[0][0] + v[1]*@itransform[1][0] + v[2]*@itransform[2][0] + @itransform[3][0], - v[0]*@itransform[0][1] + v[1]*@itransform[1][1] + v[2]*@itransform[2][1] + @itransform[3][1], - v[0]*@itransform[0][2] + v[1]*@itransform[1][2] + v[2]*@itransform[2][2] + @itransform[3][2]] - end - - def worldVector(fx, fy, tx, ty) - wfm_prime = calc_prime(screenToTarget(fx, fy)) - wto_prime = calc_prime(screenToTarget(tx, ty)) - return [wto_prime[0] - wfm_prime[0], wto_prime[1] - wfm_prime[1], wto_prime[2] - wfm_prime[2]] - end - - def spherePoint(x, y) - if @wvt.w > @wvt.h - screenmin = wvt.h.to_f - else - screenmin = wvt.w.to_f - end - v = [] - v[0] = 2.0 * (x - 0.5*@wvt.w) / screenmin - v[1] = 2.0 * (0.5 * @wvt.h - y) / screenmin - d = v[0]*v[0] + v[1]*v[1] - - if d < 0.75 - v[2] = sqrt(1.0-d) - elsif d < 3.0 - d = 1.7320508008 - sqrt(d) - t = 1.0 - d*d - t = 0.0 if t < 0.0 - v[2] = 1.0 - sqrt(t) - else - v[2] = 0.0 - end - - length = sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]) - if length > 0.0 - return [v[0] / length, v[1] / length, v[2] / length] - else - return [0.0, 0.0, 0.0] - end - end - - def turn(fx, fy, tx, ty) - return Quaternion.arc(spherePoint(fx,fy), spherePoint(tx,ty)) - end - - def mode=(mode) - @mode = mode - case @mode - when :ZOOMING - Qt::Application.setOverrideCursor(Cosmos.getCursor(Qt::SizeVerCursor)) - when :DRAGGING - Qt::Application.setOverrideCursor(Cosmos.getCursor(Qt::ClosedHandCursor)) - when :ROTATING - Qt::Application.setOverrideCursor(Cosmos.getCursor(Qt::CrossCursor)) - when :TRANSLATING - Qt::Application.setOverrideCursor(Cosmos.getCursor(Qt::SizeAllCursor)) - else - Qt::Application.restoreOverrideCursor - end - end - - def mousePressEvent(event) - case event.button - when Qt::LeftButton - self.mode = :PICKING - if (event.buttons & Qt::RightButton.to_i) != 0 - self.mode = :ZOOMING - elsif (@selection and @selection.dragable and @selection == pick(event.x, event.y)) - self.mode = :DRAGGING - end - when Qt::RightButton - if (event.buttons & Qt::LeftButton.to_i) != 0 - self.mode = :ZOOMING - else - self.mode = :POSTING - end - when Qt::MidButton - self.mode = :ZOOMING - end - @lastPos = event.pos - end - - def mouseReleaseEvent(event) - case @mode - when :PICKING - self.selection = pick(event.x, event.y) - end - - if (((event.buttons & Qt::RightButton.to_i) != 0) and ((event.buttons & Qt::LeftButton.to_i) != 0)) or ((event.buttons & Qt::MidButton.to_i) != 0) - self.mode = :ZOOMING - elsif (event.buttons & Qt::LeftButton.to_i) != 0 - self.mode = :ROTATING - elsif (event.buttons & Qt::RightButton.to_i) != 0 - self.mode = :TRANSLATING - else - self.mode = :HOVERING - end - end - - def mouseMoveEvent(event) - dx = event.x - @lastPos.x - dy = event.y - @lastPos.y - - case @mode - when :PICKING, :POSTING - if dx.abs > 0 or dy.abs > 0 - if @mode == :PICKING - self.mode = :ROTATING - else - self.mode = :TRANSLATING - end - end - when :TRANSLATING - vector = worldVector(@lastPos.x, @lastPos.y, event.x, event.y) - translate([-vector[0], -vector[1], -vector[2]]) - when :DRAGGING - if @selection and @selection.drag(self, @lastPos.x, @lastPos.y, event.x, event.y) - updateGL() - end - when :ROTATING - self.orientation = turn(@lastPos.x, @lastPos.y, event.x, event.y) * @orientation - when :ZOOMING - delta = 0.005 * dy - self.zoom = @zoom * (2.0 ** delta) - end - - @lastPos = event.pos - end - - def wheelEvent(event) - self.zoom = @zoom * (2.0 ** (-0.1 * event.delta / 120.0)) - end - - protected - - def updateProjection - # Should be non-0 size viewport - if @wvt.w > 0 and @wvt.h > 0 - # Aspect ratio of viewer - aspect = @wvt.h.to_f / @wvt.w.to_f - - # Get world box - r = 0.5 * @diameter / @zoom - if @wvt.w <= @wvt.h - @wvt.left = -r - @wvt.right = r - @wvt.bottom = -r * aspect - @wvt.top = r * aspect - else - @wvt.left = -r / aspect - @wvt.right = r / aspect - @wvt.bottom = -r - @wvt.top = r - end - - @wvt.yon = @distance + @diameter - @wvt.hither = 0.1 * @wvt.yon - - # Size of a pixel in world and model - @worldpx = (@wvt.right - @wvt.left) / @wvt.w - @modelpx = @worldpx * @diameter - - # Precalc stuff for view->world backmapping - @ax = @wvt.left - @ay = @wvt.top - @worldpx - - # Correction for perspective - if @projection == :PERSPECTIVE - hither_fac= @wvt.hither / @distance - @wvt.left *= hither_fac - @wvt.right *= hither_fac - @wvt.top *= hither_fac - @wvt.bottom *= hither_fac - end - end - end - - def updateTransform - @transform = Matrix.identity(4) - @transform.trans4(0.0, 0.0, -@distance.to_f) - @transform.rot4(@orientation); - @transform.scale4(@scale[0], @scale[1], @scale[2]); - @transform.trans4(-@center[0], -@center[1], -@center[2]); - @itransform = @transform.inverse - end - - end # class OpenGLViewer - -end # module Cosmos +# encoding: ascii-8bit + +# Copyright 2014 Ball Aerospace & Technologies Corp. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt + +# This file is inspired by the FOX Gui toolkit's FXGLViewer class + +require 'cosmos' +require 'cosmos/gui/qt' +require 'cosmos/gui/opengl/opengl' + +module Cosmos + + class GlViewer < Qt::GLWidget + MAX_PICKBUF = 1024 + MAX_SELPATH = 64 + EPS = 1.0e-2 + PICK_TOL = 3 + DTOR = 0.0174532925199432957692369077 + RTOD = 57.295779513082320876798154814 + + attr_accessor :projection # :PARALLEL or :PERSPECTIVE + attr_reader :zoom + attr_reader :fov + attr_reader :wvt + attr_reader :diameter + attr_reader :distance + attr_reader :orientation + attr_reader :center + attr_reader :scale + attr_reader :transform + attr_reader :itransform + attr_reader :maxhits + attr_reader :top_background + attr_reader :bottom_background + attr_reader :ambient + attr_reader :light + attr_reader :material + attr_reader :dropped + attr_reader :selection + attr_reader :scene + attr_reader :smode + attr_reader :options + + attr_accessor :selection_callback + attr_accessor :draw_axis + def initialize(parent) + super(parent) + + @defaultCursor = nil + @dragCursor = nil + @projection = :PERSPECTIVE + @zoom = 1.0 + @fov = 30.0 + @wvt = GlViewport.new + @diameter = 2.0; + @distance = 7.464116; + @orientation = Quaternion.new([0.0, 0.0, 0.0, 1.0]) + @center = [0.0, 0.0, 0.0] + @scale = [1.0, 1.0, 1.0] + updateProjection() + updateTransform() + @maxhits = 512; + @top_background = [0.5, 0.5, 1.0, 1.0] + @bottom_background = [1.0, 1.0, 1.0, 1.0] + @ambient = [0.2, 0.2, 0.2, 1.0] + @light = GlLight.new + @material = GlMaterial.new + @dial = [0, 0, 0] + @dropped = nil + @selection = nil + @scene = nil + @mode = :HOVERING + @draw_axis = nil + @options = [] + + @selection_callback = nil + end + + def minimumSizeHint + return Qt::Size.new(100, 100) + end + + def sizeHint + return Qt::Size.new(400, 400) + end + + def scene=(scene) + @scene = scene + @scale = [1.0, 1.0, 1.0] + if @scene + self.bounds = @scene.bounds + @zoom = @scene.zoom + @orientation = @scene.orientation + @center = @scene.center + @projection = @scene.projection + end + updateProjection() + updateTransform() + updateGL() + end + + def fov=(fov) + fov = 2.0 if fov < 2.0 + fov = 90.0 if fov > 90.0 + @fov = fov + tn = tan(0.5 * DTOR * @fov) + @distance = @diameter / tn + updateProjection() + updateTransform() + updateGL() + end + + def distance=(distance) + distance = @diameter if distance < @diameter + distance = 114.0 * @diameter if distance > (114.0 * @diameter) + if distance != @distance + @distance = distance + @fov = 2.0 * RTOD * atan2(@diameter, @distance) + updateProjection() + updateTransform() + updateGL() + end + end + + def zoom=(zoom) + zoom = 1.0e-30 if zoom < 1.0e-30 + if zoom != @zoom + @zoom = zoom + updateProjection() + updateGL() + end + end + + def scale= (scale) + scale[0] = 0.000001 if scale[0] < 0.000001 + scale[1] = 0.000001 if scale[1] < 0.000001 + scale[2] = 0.000001 if scale[2] < 0.000001 + if scale != @scale + @scale = scale + updateTransform() + updateGL() + end + end + + def orientation= (orientation) + if (orientation.q0 != @orientation.q0) or (orientation.q1 != @orientation.q1) or (orientation.q2 != @orientation.q2) or (orientation.q3 != @orientation.q3) + @orientation = orientation.clone.normalize + updateTransform() + update() + end + end + + def bounds= (bounds) + # Model center + @center = bounds.center + + # Model size + @diameter = bounds.longest + + # Fix zero size model + @diameter = 1.0 if @diameter < 1.0e-30 + + # Set equal scaling initially + @scale = [1.0, 1.0, 1.0] + + # Reset distance (and thus field of view) + self.distance = 1.1 * @diameter + end + + def center= (center) + if center != @center + @center = center + updateTransform() + updateGL() + end + end + + def selection= (shape) + @selection = shape + @selection_callback.call(shape) if @selection_callback + updateGL() + end + + def translate(vector) + @center[0] += vector[0] + @center[1] += vector[1] + @center[2] += vector[2] + updateTransform() + updateGL() + end + + def selectHits(x, y, w, h) + mh = @maxhits + nhits = 0 + makeCurrent() + + # Where to pick + pickx = (@wvt.w - 2.0*x - w) / w.to_f + picky = (2.0*y + h - @wvt.h) / h.to_f + pickw = @wvt.w / w.to_f + pickh = @wvt.h / h.to_f + + # Set pick projection matrix + GL.MatrixMode(GL::PROJECTION); + GL.LoadIdentity() + GL.Translatef(pickx, picky, 0.0) + GL.Scalef(pickw, pickh, 1.0) + case projection + when :PARALLEL + GL.Ortho(@wvt.left, @wvt.right, @wvt.bottom, @wvt.top, @wvt.hither, @wvt.yon) + when :PERSPECTIVE + GL.Frustum(@wvt.left, @wvt.right, @wvt.bottom, @wvt.top, @wvt.hither, @wvt.yon) + end + + # Model matrix + GL.MatrixMode(GL::MODELVIEW); + GL.LoadMatrixf(@transform) + + # Loop until room enough to fit + while true + nhits = 0 + buffer = GL.SelectBuffer(mh) + GL.RenderMode(GL::SELECT); + GL.InitNames() + GL.PushName(0) + @scene.hit(self) if @scene + GL.PopName() + nhits = GL.RenderMode(GL::RENDER) + mh <<= 1 + break if nhits >= 0 + end + doneCurrent() + return buffer.unpack("L*"), nhits + end + + def processHits(pickbuffer, nhits) + if nhits > 0 + zmin = 4294967295 + zmax = 4294967295 + i = 0 + while nhits > 0 + n = pickbuffer[i] + d1 = pickbuffer[1+i] + d2 = pickbuffer[2+i] + if ((d1 < zmin) || ((d1 == zmin) && (d2<=zmax))) + zmin = d1 + zmax = d2 + sel = i + end + i += n + 3 + nhits -= 1 + end + return @scene.identify(pickbuffer[4 + sel]) + end + return nil + end + + def pick(x, y) + obj = nil + if @scene and @maxhits + pickbuffer, nhits = selectHits(x-PICK_TOL, y-PICK_TOL, PICK_TOL*2, PICK_TOL*2) + obj = processHits(pickbuffer, nhits) if nhits > 0 + end + return obj; + end + + def initializeGL + GL.GetError() + + # Initialize GL context + GL.RenderMode(GL::RENDER) + + # Fast hints + GL.Hint(GL::POLYGON_SMOOTH_HINT, GL::FASTEST) + GL.Hint(GL::PERSPECTIVE_CORRECTION_HINT, GL::FASTEST) + GL.Hint(GL::FOG_HINT, GL::FASTEST) + GL.Hint(GL::LINE_SMOOTH_HINT, GL::FASTEST) + GL.Hint(GL::POINT_SMOOTH_HINT, GL::FASTEST) + + # Z-buffer test on + GL.Enable(GL::DEPTH_TEST) + GL.DepthFunc(GL::LESS) + GL.DepthRange(0.0, 1.0) + GL.ClearDepth(1.0) + GL.ClearColor(@top_background[0], @top_background[1], @top_background[2], @top_background[3]) + + # No face culling + GL.Disable(GL::CULL_FACE) + GL.CullFace(GL::BACK) + GL.FrontFace(GL::CCW) + + # Two sided lighting + GL.LightModeli(GL::LIGHT_MODEL_TWO_SIDE, 1) + GL.LightModel(GL::LIGHT_MODEL_AMBIENT, @ambient) + + # Preferred blend over background + GL.BlendFunc(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA) + + # Light on + GL.Enable(GL::LIGHT0) + GL.Light(GL::LIGHT0, GL::AMBIENT, @light.ambient) + GL.Light(GL::LIGHT0, GL::DIFFUSE, @light.diffuse) + GL.Light(GL::LIGHT0, GL::SPECULAR, @light.specular) + GL.Light(GL::LIGHT0, GL::POSITION, @light.position) + GL.Light(GL::LIGHT0, GL::SPOT_DIRECTION, @light.direction) + GL.Lightf(GL::LIGHT0, GL::SPOT_EXPONENT, @light.exponent) + GL.Lightf(GL::LIGHT0, GL::SPOT_CUTOFF, @light.cutoff) + GL.Lightf(GL::LIGHT0, GL::CONSTANT_ATTENUATION, @light.c_attn) + GL.Lightf(GL::LIGHT0, GL::LINEAR_ATTENUATION, @light.l_attn) + GL.Lightf(GL::LIGHT0, GL::QUADRATIC_ATTENUATION, @light.q_attn) + + # Viewer is close + GL.LightModeli(GL::LIGHT_MODEL_LOCAL_VIEWER, 1) + + # Material colors + GL.Material(GL::FRONT_AND_BACK, GL::AMBIENT, @material.ambient) + GL.Material(GL::FRONT_AND_BACK, GL::DIFFUSE, @material.diffuse) + GL.Material(GL::FRONT_AND_BACK, GL::SPECULAR, @material.specular) + GL.Material(GL::FRONT_AND_BACK, GL::EMISSION, @material.emission) + GL.Materialf(GL::FRONT_AND_BACK, GL::SHININESS, @material.shininess) + + # Vertex colors change both diffuse and ambient + GL.ColorMaterial(GL::FRONT_AND_BACK, GL::AMBIENT_AND_DIFFUSE) + GL.Disable(GL::COLOR_MATERIAL) + + # Simplest and fastest drawing is default + GL.ShadeModel(GL::FLAT) + GL.Disable(GL::BLEND) + GL.Disable(GL::LINE_SMOOTH) + GL.Disable(GL::POINT_SMOOTH) + GL.Disable(GL::COLOR_MATERIAL) + + # Lighting + GL.Disable(GL::LIGHTING) + + # No normalization of normals (it's broken on some machines anyway) + GL.Disable(GL::NORMALIZE) + + # Dithering if needed + GL.Disable(GL::DITHER) + end + + def paintGL + # Set viewport + GL.Viewport(0, 0, @wvt.w, @wvt.h) + + # Reset important stuff + GL.ShadeModel(GL::SMOOTH) + GL.PolygonMode(GL::FRONT_AND_BACK, GL::FILL) + GL.Disable(GL::LIGHTING) + GL.Disable(GL::ALPHA_TEST) + GL.Disable(GL::BLEND) + GL.Disable(GL::DITHER) + GL.Disable(GL::FOG) + GL.Disable(GL::LOGIC_OP) + GL.Disable(GL::POLYGON_SMOOTH) + GL.Disable(GL::POLYGON_STIPPLE) + GL.Disable(GL::STENCIL_TEST) + GL.Disable(GL::CULL_FACE) + GL.Disable(GL::COLOR_MATERIAL) + + # Reset matrices + GL.MatrixMode(GL::PROJECTION) + GL.LoadIdentity + GL.MatrixMode(GL::MODELVIEW) + GL.LoadIdentity + + # Clear to solid background + GL.ClearDepth(1.0) + GL.ClearColor(@top_background[0], @top_background[1], @top_background[2], @top_background[3]) + if @top_background == @bottom_background + begin + GL.Clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT) + rescue + # Raises false error on Mac + end + else # Clear to gradient background + begin + GL.Clear(GL::DEPTH_BUFFER_BIT) + rescue + # Raises false error on Mac + end + GL.Disable(GL::DEPTH_TEST) + GL.DepthMask(GL::FALSE) + GL.Begin(GL::TRIANGLE_STRIP) + GL.Color(@bottom_background); GL.Vertex3f(-1.0, -1.0, 0.0); GL.Vertex3f(1.0, -1.0, 0.0) + GL.Color(@top_background); GL.Vertex3f(-1.0, 1.0, 0.0); GL.Vertex3f(1.0, 1.0, 0.0) + begin + GL.End + rescue + # Raises false error on Mac + end + end + + # Depth test on by default + GL.DepthMask(GL::TRUE) + GL.Enable(GL::DEPTH_TEST) + + # Switch to projection matrix + GL.MatrixMode(GL::PROJECTION) + GL.LoadIdentity + case @projection + when :PARALLEL + GL.Ortho(@wvt.left, @wvt.right, @wvt.bottom, @wvt.top, @wvt.hither, @wvt.yon) + when :PERSPECTIVE + GL.Frustum(@wvt.left, @wvt.right, @wvt.bottom, @wvt.top, @wvt.hither, @wvt.yon) + end + + # Switch to model matrix + GL.MatrixMode(GL::MODELVIEW) + GL.LoadIdentity + + # Set light parameters + GL.Enable(GL::LIGHT0) + GL.Light(GL::LIGHT0, GL::AMBIENT, @light.ambient) + GL.Light(GL::LIGHT0, GL::DIFFUSE, @light.diffuse) + GL.Light(GL::LIGHT0, GL::SPECULAR, @light.specular) + GL.Light(GL::LIGHT0, GL::POSITION, @light.position) + GL.Light(GL::LIGHT0, GL::SPOT_DIRECTION, @light.direction) + GL.Lightf(GL::LIGHT0, GL::SPOT_EXPONENT, @light.exponent) + GL.Lightf(GL::LIGHT0, GL::SPOT_CUTOFF, @light.cutoff) + GL.Lightf(GL::LIGHT0, GL::CONSTANT_ATTENUATION, @light.c_attn) + GL.Lightf(GL::LIGHT0, GL::LINEAR_ATTENUATION, @light.l_attn) + GL.Lightf(GL::LIGHT0, GL::QUADRATIC_ATTENUATION, @light.q_attn) + + # Default material colors + GL.Material(GL::FRONT_AND_BACK, GL::AMBIENT, @material.ambient) + GL.Material(GL::FRONT_AND_BACK, GL::DIFFUSE, @material.diffuse) + GL.Material(GL::FRONT_AND_BACK, GL::SPECULAR, @material.specular) + GL.Material(GL::FRONT_AND_BACK, GL::EMISSION, @material.emission) + GL.Materialf(GL::FRONT_AND_BACK, GL::SHININESS, @material.shininess) + + # Color commands change both + GL.ColorMaterial(GL::FRONT_AND_BACK, GL::AMBIENT_AND_DIFFUSE) + + # Global ambient light + GL.LightModel(GL::LIGHT_MODEL_AMBIENT, @ambient) + + # Enable fog + if @options.include?(:VIEWER_FOG) + GL.Enable(GL::FOG) + GL.Fog(GL::FOG_COLOR, @top_background) # Disappear into the background + GL.Fogf(GL::FOG_START, (@distance - @diameter).to_f) # Range tight around model position + GL.Fogf(GL::FOG_END, (@distance + @diameter).to_f) # Far place same as clip plane:- clipped stuff is in the mist! + GL.Fogi(GL::FOG_MODE, GL::LINEAR) # Simple linear depth cueing + end + + # Dithering + GL.Enable(GL::DITHER) if @options.include?(:VIEWER_DITHER) + + # Enable lighting + GL.Enable(GL::LIGHTING) if @options.include?(:VIEWER_LIGHTING) + + # Set model matrix + GL.LoadMatrixf(@transform) + + if (@draw_axis and @draw_axis > 0) + # Draw axis + GL.PushMatrix + GL.LineWidth(2.5) + GL.Color3f(1.0, 0.0, 0.0) + GL.Begin(GL::LINES) + GL.Vertex3f(-@draw_axis.to_f, 0.0, 0.0) + GL.Vertex3f(@draw_axis.to_f, 0.0, 0.0) + begin + GL.End + rescue + # Raises false error on Mac + end + GL.Color3f(0.0, 1.0, 0.0) + GL.Begin(GL::LINES) + GL.Vertex3f(0, -@draw_axis, 0.0) + GL.Vertex3f(0, @draw_axis, 0) + begin + GL.End + rescue + # Raises false error on Mac + end + GL.Color3f(0.0, 0.0, 1.0) + GL.Begin(GL::LINES) + GL.Vertex3f(0, 0, -@draw_axis) + GL.Vertex3f(0, 0, @draw_axis) + begin + GL.End + rescue + # Raises false error on Mac + end + GL.PopMatrix + end + + # Draw what's visible + @scene.draw(self) if @scene + end + + def resizeGL(width, height) + @wvt.w = width; + @wvt.h = height; + updateProjection() + end + + def screenToEye(sx, sy, eyez) + e = [0.0, 0.0, 0.0] + xp = (@worldpx*sx + @ax).to_f + yp = (@ay - @worldpx*sy).to_f + if @projection == :PERSPECTIVE + if @distance != 0.0 + e.x = [((-eyez*xp) / @distance).to_f, ((-eyez*yp) / @distance).to_f, eyez] + end + else + e = [xp, yp, eyez] + end + return e; + end + + def screenToTarget(sx, sy) + [@worldpx*sx.to_f + @ax, @ay - @worldpx*sy.to_f, -@distance.to_f] + end + + def eyeToWorld(e) + [e[0]*@itransform[0][0] + e[1]*@itransform[1][0] + e[2]*@itransform[2][0] + @itransform[3][0], + e[0]*@itransform[0][1] + e[1]*@itransform[1][1] + e[2]*@itransform[2][1] + @itransform[3][1], + e[0]*@itransform[0][2] + e[1]*@itransform[1][2] + e[2]*@itransform[2][2] + @itransform[3][2]] + end + + def worldToEyeZ(w) + w[0]*@transform[0][2] + w[1]*@transform[1][2] + w[2]*@transform[2][2] + @transform[3][2] + end + + def calc_prime(v) + return [v[0]*@itransform[0][0] + v[1]*@itransform[1][0] + v[2]*@itransform[2][0] + @itransform[3][0], + v[0]*@itransform[0][1] + v[1]*@itransform[1][1] + v[2]*@itransform[2][1] + @itransform[3][1], + v[0]*@itransform[0][2] + v[1]*@itransform[1][2] + v[2]*@itransform[2][2] + @itransform[3][2]] + end + + def worldVector(fx, fy, tx, ty) + wfm_prime = calc_prime(screenToTarget(fx, fy)) + wto_prime = calc_prime(screenToTarget(tx, ty)) + return [wto_prime[0] - wfm_prime[0], wto_prime[1] - wfm_prime[1], wto_prime[2] - wfm_prime[2]] + end + + def spherePoint(x, y) + if @wvt.w > @wvt.h + screenmin = wvt.h.to_f + else + screenmin = wvt.w.to_f + end + v = [] + v[0] = 2.0 * (x - 0.5*@wvt.w) / screenmin + v[1] = 2.0 * (0.5 * @wvt.h - y) / screenmin + d = v[0]*v[0] + v[1]*v[1] + + if d < 0.75 + v[2] = sqrt(1.0-d) + elsif d < 3.0 + d = 1.7320508008 - sqrt(d) + t = 1.0 - d*d + t = 0.0 if t < 0.0 + v[2] = 1.0 - sqrt(t) + else + v[2] = 0.0 + end + + length = sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]) + if length > 0.0 + return [v[0] / length, v[1] / length, v[2] / length] + else + return [0.0, 0.0, 0.0] + end + end + + def turn(fx, fy, tx, ty) + return Quaternion.arc(spherePoint(fx,fy), spherePoint(tx,ty)) + end + + def mode=(mode) + @mode = mode + case @mode + when :ZOOMING + Qt::Application.setOverrideCursor(Cosmos.getCursor(Qt::SizeVerCursor)) + when :DRAGGING + Qt::Application.setOverrideCursor(Cosmos.getCursor(Qt::ClosedHandCursor)) + when :ROTATING + Qt::Application.setOverrideCursor(Cosmos.getCursor(Qt::CrossCursor)) + when :TRANSLATING + Qt::Application.setOverrideCursor(Cosmos.getCursor(Qt::SizeAllCursor)) + else + Qt::Application.restoreOverrideCursor + end + end + + def mousePressEvent(event) + case event.button + when Qt::LeftButton + self.mode = :PICKING + if (event.buttons & Qt::RightButton.to_i) != 0 + self.mode = :ZOOMING + elsif (@selection and @selection.dragable and @selection == pick(event.x, event.y)) + self.mode = :DRAGGING + end + when Qt::RightButton + if (event.buttons & Qt::LeftButton.to_i) != 0 + self.mode = :ZOOMING + else + self.mode = :POSTING + end + when Qt::MidButton + self.mode = :ZOOMING + end + @lastPos = event.pos + end + + def mouseReleaseEvent(event) + case @mode + when :PICKING + self.selection = pick(event.x, event.y) + end + + if (((event.buttons & Qt::RightButton.to_i) != 0) and ((event.buttons & Qt::LeftButton.to_i) != 0)) or ((event.buttons & Qt::MidButton.to_i) != 0) + self.mode = :ZOOMING + elsif (event.buttons & Qt::LeftButton.to_i) != 0 + self.mode = :ROTATING + elsif (event.buttons & Qt::RightButton.to_i) != 0 + self.mode = :TRANSLATING + else + self.mode = :HOVERING + end + end + + def mouseMoveEvent(event) + dx = event.x - @lastPos.x + dy = event.y - @lastPos.y + + case @mode + when :PICKING, :POSTING + if dx.abs > 0 or dy.abs > 0 + if @mode == :PICKING + self.mode = :ROTATING + else + self.mode = :TRANSLATING + end + end + when :TRANSLATING + vector = worldVector(@lastPos.x, @lastPos.y, event.x, event.y) + translate([-vector[0], -vector[1], -vector[2]]) + when :DRAGGING + if @selection and @selection.drag(self, @lastPos.x, @lastPos.y, event.x, event.y) + updateGL() + end + when :ROTATING + self.orientation = turn(@lastPos.x, @lastPos.y, event.x, event.y) * @orientation + when :ZOOMING + delta = 0.005 * dy + self.zoom = @zoom * (2.0 ** delta) + end + + @lastPos = event.pos + end + + def wheelEvent(event) + self.zoom = @zoom * (2.0 ** (-0.1 * event.delta / 120.0)) + end + + protected + + def updateProjection + # Should be non-0 size viewport + if @wvt.w > 0 and @wvt.h > 0 + # Aspect ratio of viewer + aspect = @wvt.h.to_f / @wvt.w.to_f + + # Get world box + r = 0.5 * @diameter / @zoom + if @wvt.w <= @wvt.h + @wvt.left = -r + @wvt.right = r + @wvt.bottom = -r * aspect + @wvt.top = r * aspect + else + @wvt.left = -r / aspect + @wvt.right = r / aspect + @wvt.bottom = -r + @wvt.top = r + end + + @wvt.yon = @distance + @diameter + @wvt.hither = 0.1 * @wvt.yon + + # Size of a pixel in world and model + @worldpx = (@wvt.right - @wvt.left) / @wvt.w + @modelpx = @worldpx * @diameter + + # Precalc stuff for view->world backmapping + @ax = @wvt.left + @ay = @wvt.top - @worldpx + + # Correction for perspective + if @projection == :PERSPECTIVE + hither_fac= @wvt.hither / @distance + @wvt.left *= hither_fac + @wvt.right *= hither_fac + @wvt.top *= hither_fac + @wvt.bottom *= hither_fac + end + end + end + + def updateTransform + @transform = Matrix.identity(4) + @transform.trans4(0.0, 0.0, -@distance.to_f) + @transform.rot4(@orientation); + @transform.scale4(@scale[0], @scale[1], @scale[2]); + @transform.trans4(-@center[0], -@center[1], -@center[2]); + @itransform = @transform.inverse + end + + end # class OpenGLViewer + +end # module Cosmos