
Object subclass: #ThreeDObject
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: '' !


!ThreeDObject class methods !

comment
    "
Introduction

  Microsoft's new 32 bit operating system, Windows NT comes equipped with OpenGL,
originally developed by Silicon Graphics, Inc., for displaying advanced 3-D graphics.
OpenGL (Open Graphics Library) is a software standard that can be implemented on
any platform. Microsoft's OpenGL provides a library of about 120 commands that allows
users to build up 3D models from a small set of geometric primitives such as points,
lines, and polygons, lights, and textures. The facilities are sufficient to permit relatively
sophisticated objects to be built and rendered. It does not, however, provide window,
mouse, or keyboard facilities nor does it provide built-in 3D objects although there are
utility and auxiliary libraries for OpenGL that do.

Coordinates and Transformations

    The coordinate system supported by OpenGL is the familiar x is positive to the right,
y positive up, and z positive out of the screen. A point can be viewed as a row vector
[x, y, z, w]. The 4th coordinate can be adjusted so that it is always 1 so it need never be
stored. So, a point can be written [x, y, z] as a short form for [x, y, z, 1].

    A transformation T is a 4 by 4 matrix that can be applied to a point [x, y, z, w] to
transform it to a new point [x', y', z', w'] as follows:

    [x', y', z', w'] = T * [x, y, z, w]

    Believe it or not, it's not necessary to know what's inside a transformation to use it as
long as the transformation to do the desired job is obtainable. For example, an identity
transformation applied to  [x, y, z, w] will just give back  [x, y, z, w]. But transformations
too can be transformed by either translating them, scaling them, or rotating them about
another point. For example, consider a transformation T1 obtained by translating an
identity transformation by [5,10,-4]. When T1 is applied to [x, y, z], the result is
[x+5, y+10, z-4]. If another transformation T2 is translated by [100,0,0], then applying
T2 to [x+5, y+10, z-4] should give [x+105, y+10, z-4]. But transformations can also be
multiplied. If T3 is T1 * T2, then T3 applied to [x, y, z] will also give [x+105, y+10, z-4].
More generally, any number of transformations can be multiplied together but they are
applied from right to left in OpenGL as follows:

    T * [x, y, z, w] = (T1 * T2 * T3 * ... Tn) * [x, y, z, w] = T1 * (T2 * (T3 * ... (Tn * [x, y, z, w])))

    In the simple case where n = 2, the last transformation T2 can be associated with a 3D
object. In that case, it can be viewed as mapping a point in local coordinates to more
global world coordinates. If the first transformation T1 is associated with a camera, this
transformation can further map this global coordinates to coordinates as seen from the
perspective of the camera. This will happen naturally if the movement of the
camera is defined as changing its matrix in the opposite direction.

    For example, suppose T2 maps [0,0,0] (the object's origin) to [5,0,-5]; i.e., to the right
and forward 5 units. If the camera is moved forward one unit, it is being moved in the
direction [0,0,-1]. If the matrix is translated by [0,0,+1], then T1 * T2 will move the object's
origin [0,0,0] through [5,0,-5] to [5,0,-4]; i.e., to the right of the camera and only 4 units
away. So the object appears closer.

    If another transformation is introduced in between so that T1 is the camera, T2 is a
solar system (that translates by [0,0,-100]), and T3 is an object in the solar system. Then
translating T1 * T2 * T3 can be interpreted as translating the object by [5,0,-5] within the
solar system which is itself translated by [0,0,-100] to move it very far away, which is
finally brought a little closer by moving the camera forward which translates by [0,0,1].

In general, pre-multiplying T by a rotation R versus post-multiplying it gives us totally
different effects. For example, if T1 is the camera and T2 is the object, then

    T1 * T2 * R has the effect of rotating the object
    R * T1 * T2 has the effect of rotating the camera

    Because of this major difference, post-multiplication can be viewed as locally
transforming (the more extensively used variant) while pre-multiplication is globally
transforming.

    The 3D object classes consist of transformable objects and coatings. A transformable
object maintains (1) a transformation matrix that controls positioning, rotation, and
scaling and (2) an animation block that supports controlled changes each time the
object is rendered. Coatings provide details about the surface of the objects; either
a paint coating (just a color) or a texture coating (which maps a 2D bitmap onto the
surface). What is needed is a camera (which controls what you see in a scene),
at least one light (lights are not visible although they do affect the visibility of other
objects), and either a cube or a maze (only the latter two are solid objects). Mazes are
constructed from collections of cubes and cubes in turn from collections of faces. All
solid objects have a coating which if unspecified (i.e., a null coating) implies that it is
inherited from the container object. For example, if a cube is contructed from five faces
with null coatings and one face with a green paint, the faces with null coatings can be
rendered in red by setting the cube's coating to red paint. If we further made a shallow
copy of the cube and set the paint of the copy to yellow, the two cubes would render with
five red and yellow sides respectively.

Rendering Three Dimensional Objects

    In anticipation of future extensions, the rendering process has been separated into
three parts using the following messages:

1.  'beginRenderOn: aPane' for rendering commands that must ultimately be 'undone'; e.g.,
    commands such as pushMatrix.
2.  'renderOn: aPane' for the standard object rendering commands.
3.  'endRenderOn: aPane' for undoing the effects of  'beginRenderOn: aPane'; e.g. popMatrix.

Class ThreeDObject provides the basic framework in method basicRenderOn: (where each of
the three rendering methods do nothing). Transformable objects push their transformation
matrix  on the OpenGL stack at the beginning (but first, the animation block is executed; it
could change the color or transformation matrix, for example) and pop it off at the end.
Subclasses do the actual rendering work. Rendering commands must be generated for the
camera first, then commands for the lights, and finally commands for the objects. For a camera,
a maze, and one cube in the maze, The camera matrix, the maze matrix, and then the cube matrix
must be pushed in the OpenGL matrix stack successively applying each new matrix to the
previous one (without modifying the previous one) so that we can later unstack the new
matrix to undo all the changes.

    Cameras are rendered by executing special setup messages and by loading their
transformation matrix. A light is rendered by enabling lighting, setting the diffuse,
ambient, and specular components, setting its direction and optionally its position.
Up to eight lights are possible, each with a different identifier.

    Solid objects render their coating with the basicRenderOn: message. Subclasses
additionally do more. Composite objects, in particular, push their coating on the
OpenGL stack (for eventual use by components that have null coatings) and then
pop it back off the stack when done. Push and pop really work as expected for paint
and texture coatings but do nothing for null coatings. The basic rendering cycle is also
applied to each component. Maze and Cube inherits all of its rendering behavior from the
CompositeObject class. So all the real work is done by coatings and faces.

A paint coating must disable texture mapping and provide the color whereas a texture
coating must enable it and select the texture coating (which really provides OpenGL
with all the bits in the 2D bitmap; A special converter was writen to convert 'BMP' files
to 'fix' the width and height of the bitmaps to powers of two, as required by OpenGL).
Selecting a texture coating is very expensive because the pixels are converted by
OpenGL to a private format as it is moved from Smalltalk space to OpenGL space.
32 bit pixels are used since it is much easier to deal with OpenGL if palettes are
avoided. Lights and shadows also don't work very well with palettes. To minimize
the expense of selecting textures, OpenGLLibrary was extended to maintain a stack
of coatings in the current OpenGL pen and also to keep track of the last rendered
coating (the current coating). When a non-null coating is to be rendered, is is compared
with the current coating. If it is the same, it is not re-render. If it is different, it is
rendered and the current coating is set to this one. When a null coating is to be rendered,
the top coating on the coating stack is used in its place (remember, this stack
only contains non-null coatings).

Faces are rendered by providing OpenGL with the 3D points in the polygon along with
the corresponding points in the 2D bitmap (these are referred to as the texture vertices).
Texture vertices are always supplied allowing us to dynamically switch between paint
and texture coatings. Normally, texture vertices are the 2D points 0@0, 1@0, 1@1, 0@1
(the corner points in the clockwise direction). Such texture vertices cause the 2D bitmap
to overlay the 3D plane of the face with appropriate stretching and squishing to cover the
surface exactly. Texture coatings also have a ratio which defaults to 1@1 to provide the
above texture vertices. When a ratio such as (1/2)@4 is supplied with a texture coating,
the effect is as follows: (1) in the x-direction, only half of the bitmap is seen in the face
(it appears larger); (2) in the y-direction,  the bitmap is replicated 4 times in the face
(so that it appears smaller).

Mazes and Cubes

    Composite objects have obvious methods for adding objects; addLight: (which adds to
the front; nothing would be lit it the lights were added after all the objects) and
add3DObject: (which adds to the rear).

    Mazes are constructed with a simple two dimensional representation that uses 0 to represent
an open space (the floor) and integers 1, 2, 3, ... to represent a closed space (a cube). The
number is an index to the texture coating that is to be mapped to the cube. One large floor is
created for the entire maze and then cubes are created for each closed area and positioned
on top of the floor. A ceiling is not created.

    Cubes are constructed by physically creating six 1-by-1 faces surrounding
the center of the cube which is at the origin. The maze uses these unit cubes directly for
the closed areas but it uses a scaled cube for the floor. Method example1 creates such
a cube and provides it with an animation block that causes it to rotate around a line that
is diagonal to the x- and y-axis (specifically, the line from [0,0,0] to [1,1,0]). So it appears
to be tumbling.

    "!
 
new
    ^super new initialize! !



!ThreeDObject methods !

basicRenderOn: aPane
    self beginRenderOn: aPane.
    self renderOn: aPane.
    self endRenderOn: aPane.!
  
beginRenderOn: aPane
    "Do nothing."!

endRenderOn: aPane
    "Do nothing."!
  
initialize
    "Do nothing."!
  
renderOn: aPane
    "Do nothing."! !


ThreeDObject subclass: #Coating
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: ''   !


!Coating class methods ! !



!Coating methods !
 
basicRenderOn: aPane
    self == OpenGLLibrary currentCoating ifFalse: [
        super basicRenderOn: aPane.
        OpenGLLibrary currentCoating: self].!
   
pop
    OpenGLLibrary popCoating!
  
push
    OpenGLLibrary pushCoating: self!
  
textureVertices
    ^#((0.0 0.0) (1.0 0.0) (1.0 1.0) (0.0 1.0))! !


Coating subclass: #NullCoating
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: ''  !


!NullCoating class methods ! !



!NullCoating methods !
 
basicRenderOn: aPane
        OpenGLLibrary topCoating basicRenderOn: aPane.!
   
pop
    "Do nothing."!
 
push
    "Do nothing."!

textureVertices
    ^OpenGLLibrary topCoating textureVertices! !


Coating subclass: #PaintCoating
  instanceVariableNames: 
    ' color '
  classVariableNames: ''
  poolDictionaries: 
    ' OpenGLConstants '   !


!PaintCoating class methods ! !



!PaintCoating methods !
   
color
    ^color!
  
color: colorVector
    color := colorVector!
   
initialize
    super initialize.
    self color: #(1.0 1.0 1.0 1.0).!
 
renderOn: aPane
    super renderOn: aPane.
    OpenGLLibrary
        disableTextureMapping;
        color: self color;
        materialTracksColor.! !


Coating subclass: #TextureCoating
  instanceVariableNames: 
    ' format type width height pixels2D ratio '
  classVariableNames: ''
  poolDictionaries: 
    ' OpenGLConstants ' !


!TextureCoating class methods !
  
fromBitmap: aBitmap
    ^self new fromBitmap: aBitmap!
 
fromFile: aFileName
    | bitmap texture |
    texture := self fromBitmap: (bitmap := Bitmap fromFile: aFileName).
    bitmap release.
    ^texture!

fromWindowsFile: aFileName
    | string |
    KernelLibrary getWindowsDirectory: (string := (String new: 40) asParameter) length: 39.
    ^self fromFile: string trimBlanks, '\', aFileName! !



!TextureCoating methods !
  
format
    ^format!

fromBitmap: aBitmap
    "Create a texture from the given bitmap"
    | bitmap scale temporary |

   "The width and height of the texture must be a power of two and the minimum size is 64"
    height := aBitmap height roundToPower: 2.
    width := aBitmap width roundToPower: 2.
    height < 64 ifTrue: [
        scale := 64 / height.
        width := width * scale.
        height := height * scale].
    width < 64 ifTrue: [
        scale := 64 / width.
        width := width * scale.
        height := height * scale].

    "Make a copy of the original bitmap to ensure that the bit count is 32"
    bitmap := Bitmap width: width height: height planes: 1 bitCount: 32.
    bitmap pen copyBitmap: aBitmap from: aBitmap boundingBox to: bitmap boundingBox.

    "Set the initial values of the receiver"
    format := GLRGBA.
    type := GLUnsignedByte.
    width := bitmap width.
    height := bitmap height.
    pixels2D := bitmap getDIBits.

    bitmap release.

    1 to: pixels2D size by: 4 do: [:index|
        temporary := pixels2D at: index.
        pixels2D at: index put: (pixels2D at: index + 2).
        pixels2D at: index + 2 put: temporary].
"
    1 to: pixels2D size by: 4 do: [:index|
        temporary := pixels2D at: index.
        pixels2D at: index put: (pixels2D at: index + 3).
        pixels2D at: index + 3 put: temporary.
        temporary := pixels2D at: index + 2.
        pixels2D at: index + 2 put: (pixels2D at: index + 1).
        pixels2D at: index + 1 put: temporary]."!
  
height
    ^height!

initialize
    super initialize.
    self ratio: 1 @ 1.!
  
pixels2D
    ^pixels2D!

ratio
    ^ratio!
  
ratio: aPoint
    ratio := aPoint!
 
renderOn: aPane
    super renderOn: aPane.
    OpenGLLibrary enableTextureMapping; selectTexture: self.!
  
textureVertices
    ^Array
        with: #(0.0 0.0)
        with: (Array with: 1.0 * self ratio x with: 0.0)
        with: (Array with: 1.0 * self ratio x with: 1.0 * self ratio y)
        with: (Array with: 0.0 with: 1.0 * self ratio y).!

type
    ^type!

width
    ^width! !


ThreeDObject subclass: #Transformable3DObject
  instanceVariableNames: 
    ' matrix animationBlock '
  classVariableNames: ''
  poolDictionaries: '' !


!Transformable3DObject class methods ! !



!Transformable3DObject methods !
 
animationBlock
    ^animationBlock!

animationBlock: aBlock
    animationBlock := aBlock!
   
beginRenderOn: aPane
    "Perform the animation block before loading the object's matrix since the
    animation block may change the matrix."
    super beginRenderOn: aPane.
    self animationBlock value.
    OpenGLLibrary selectModelViewMatrix; pushMatrix; multiplyMatrix: self matrix.!
   
endRenderOn: aPane
    super endRenderOn: aPane.
    OpenGLLibrary popMatrix.!

initialize
    super initialize.
    self animationBlock: [];
        matrix: OpenGLTransformation identity.!

matrix
    ^matrix!

matrix: aMatrix
    ^matrix := aMatrix!

rotateBy: angle around: a3DPoint
    self matrix rotateBy: angle around: a3DPoint!
 
scaleBy: a3DPoint
    self matrix scaleBy: a3DPoint!
   
translateBy: a3DPoint
    self matrix translateBy: a3DPoint! !


Transformable3DObject subclass: #Camera
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: 
    ' OpenGLConstants '  !


!Camera class methods ! !



!Camera methods !
   
basicRenderOn: aPane
    OpenGLLibrary
        enableDepthTest;
        clearColorAndDepthBuffers;
        selectModelViewMatrix;
        loadMatrix: self matrix.!

initialize
    super initialize.!
  
initializePerspectiveFor: aPane
    OpenGLLibrary
        selectProjectionMatrix;
        loadIdentityMatrix;
        perspective: 15.0 aspect: aPane graphicsTool aspectRatio front: 1.0 back: 1000.0;
        viewport: aPane graphicsTool boundingBox.!
 
rotateBy: angle around: a3DPoint
    self matrix globalRotateBy: angle negated around: a3DPoint!
   
translateBy: a3DPoint
    self matrix globalTranslateBy: a3DPoint as3DPoint negated! !


Transformable3DObject subclass: #Light
  instanceVariableNames: 
    ' identifier position direction color percentAmbient '
  classVariableNames: ''
  poolDictionaries: 
    ' OpenGLConstants ' !


!Light class methods ! !



!Light methods !
 
ambientColor
    ^self color collect: [:value |
        value * self percentAmbient]!
 
color
    ^color!
  
color: colorVector
    color := colorVector!
   
direction
    ^direction!
  
direction: directionVector
    direction := directionVector!
   
directional
    ^self position isNil!
  
identifier
    ^identifier!

initialize
    super initialize.
    self color: #(1.0 1.0 1.0 1.0).
    self percentAmbient: 0.0.!
  
percentAmbient
    ^percentAmbient!

percentAmbient: percent
    percentAmbient := percent!
 
position
    ^position!

position: a3DPoint
    position := a3DPoint!
   
renderOn: aPane
    super renderOn: aPane.
    self identifier isNil ifTrue: [identifier := aPane graphicsTool nextAvailableLightIdentifier].
    OpenGLLibrary
        enableLighting;
        enable: self identifier;
        light: self identifier type: GLDiffuse parameter: self color;
        light: self identifier type: GLAmbient parameter: self ambientColor;
        light: self identifier type: GLSpecular parameter: self specularColor.
    self directional
        ifTrue: [
            OpenGLLibrary light: self identifier type: GLPosition parameter: self direction , #(0.0)]
        ifFalse: [
            OpenGLLibrary
                light: self identifier type: GLPosition parameter: self position , #(1.0);
                light: self identifier type: GLSpotDirection parameter: self direction].!

specularColor
    ^color! !


Transformable3DObject subclass: #Solid3DObject
  instanceVariableNames: 
    ' coating '
  classVariableNames: ''
  poolDictionaries: ''  !

Solid3DObject subclass: #Face
  instanceVariableNames:
    ' vertices normal '
  classVariableNames: ''
  poolDictionaries: '' !


!Solid3DObject class methods ! !



!Solid3DObject methods !
 
coating
    ^coating!
  
coating: aCoating
    coating := aCoating!
 
initialize
    super initialize.
    self coating: NullCoating new.!
  
renderOn: aPane
    super renderOn: aPane.
    self coating basicRenderOn: aPane.! !


Solid3DObject subclass: #Composite3DObject
  instanceVariableNames: 
    ' objects '
  classVariableNames: ''
  poolDictionaries: ''    !


!Composite3DObject class methods ! !



!Composite3DObject methods !
 
add3DObject: a3DObject
    objects addLast: a3DObject!
 
addLight: aLight
    objects addFirst: aLight!
 
beginRenderOn: aPane
    self coating push.
    super beginRenderOn: aPane!
   
endRenderOn: aPane
    super endRenderOn: aPane.
    self coating pop.!
   
initialize
    super initialize.
    objects := OrderedCollection new.!
   
renderOn: aPane
    super renderOn: aPane.
    objects do: [:a3DObject |
        a3DObject basicRenderOn: aPane].! !


Composite3DObject subclass: #Cube
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: ''   !


!Cube class methods !

example1
    "(Cube example1)"
    | cube |
    ^(cube :=self new)
        coating: (TextureCoating fromFile: 'st.bmp');
        translateBy: #(0.0 0.0 -6.0);
        animationBlock: [cube rotateBy: 15.0 around: #(1.0 1.0 0.0)];
        yourself!
   
example2
    "(Cube example2)"
    ^Composite3DObject new
        addLight: (Light new direction: #(1.0 1.0 1.0); position: #(5.0 5.0 5.0));
        add3DObject: Cube example1;
        yourself! !



!Cube methods !

initialize

    | normals faces vertices |

    super initialize.

    normals := #(
        (-1.0 0.0 0.0) (0.0 1.0 0.0) (1.0 0.0 0.0)
        (0.0 -1.0 0.0) (0.0 0.0 1.0) (0.0 0.0 -1.0)).
    faces :=  #(
        ( 1 2 3 4 ) ( 4 3 7 8 ) ( 8 7 6 5 )
        ( 2 1 5 6 ) ( 2 6 7 3 ) ( 5 1 4 8 )).
    vertices :=  #(
        ( -0.5  -0.5  -0.5) ( -0.5  -0.5   0.5) ( -0.5   0.5   0.5) ( -0.5   0.5  -0.5)
        ( 0.5   -0.5  -0.5) ( 0.5   -0.5   0.5) ( 0.5    0.5   0.5) ( 0.5    0.5  -0.5)).

    faces with: normals do: [:face :normal|
        self add3DObject: (Face new
            normal: normal;
            vertices: (face collect: [:index | vertices at: index]))]! !


Composite3DObject subclass: #Maze
  instanceVariableNames: ''
  classVariableNames: ''
  poolDictionaries: ''   !


!Maze class methods !

example1
        "(Maze example1)"

    ^self on: #(
        (1 1 1 1 1 1 1 1 )
        (1 0 0 0 0 0 0 1 )
        (1 0 1 0 1 1 1 1 )
        (1 0 1 1 1 0 0 1 )
        (1 0 0 0 0 0 1 1 )
        (1 1 1 1 1 0 1 1 )
        (1 0 0 0 0 0 0 1 )
        (1 1 1 1 1 1 1 1 ))
        using: (Array
            with: ((TextureCoating fromFile: 'rivets.bmp') ratio: 16@16)
            with: ((TextureCoating fromFile: 'castle.bmp') ratio: 2@2))!
 
example2
    "(Maze example2)"
    ^Composite3DObject new
        addLight: (Light new direction: #(1.0 1.0 1.0));
        add3DObject: Maze example1;
        translateBy: #(0.75 0.0 -30.0);
        rotateBy: 50.0 around: #(0.0 1.0 0.0);
        rotateBy: 20.0 around: #(1.0 0.0 0.0);
        translateBy: #(4.0 0.0 4.0);
        yourself!

on: cellEncoding using: textureFiles
    ^self new on: cellEncoding using: textureFiles! !



!Maze methods !
  
addFloor: length and: width using: aCoating
    self add3DObject: (Cube new
        coating: aCoating;
        translateBy: (OpenGL3DPoint x: -1 * (length / 2) - 0.5 y: -0.05 z: -1 * (width / 2) - 0.5);
        scaleBy: (OpenGL3DPoint x: length y: 0.1 z: width)).!

addWallAt: row and: column using: aCoating
    self add3DObject: (Cube new
        coating: aCoating;
        translateBy: (OpenGL3DPoint x: -1 * row y: 0.5 z: -1 * column))!
   
on: cellEncoding using: textures
    | encoding length width |
    length := cellEncoding size.
    width := (cellEncoding at: 1) size.
    self addFloor: length and: width using: (textures at: 1).
    1 to: length do: [:row|
        1 to: width do: [:column|
            encoding := ((cellEncoding at: row) at: column).
            encoding = 0
                ifFalse: [self addWallAt: row and: column using: (textures at: encoding + 1)]]].! !


Solid3DObject subclass: #Face
  instanceVariableNames: 
    ' vertices normal '
  classVariableNames: ''
  poolDictionaries: '' !


!Face class methods !

example1
    "(Face example1)"
    ^Composite3DObject new
        coating: (TextureCoating fromFile: 'color.bmp');
        add3DObject: (Face new
            translateBy: #(0.0 0.0 -0.5));
        add3DObject: (Face new
            translateBy: #(0.0 0.0 0.5));
        translateBy: #(-0.5 -0.5 -8.0);
        rotateBy: 30.0 around: #(1.0 1.0 0.0);
        yourself! !



!Face methods !
   
initialize
    super initialize.
    self normal: #(0.0 0.0 1.0).
    self vertices:  #((0.0 0.0 0.0) (1.0 0.0 0.0) (1.0 1.0 0.0) (0.0 1.0 0.0)).!
   
normal
    ^normal!

normal: a3DPoint
    normal := a3DPoint!
   
renderOn: aPane
    super renderOn: aPane.
    OpenGLLibrary beginPolygon;
        normal: self normal.
        self coating textureVertices with: self vertices do: [:textureVertex :vertex |
            OpenGLLibrary
                textureVertex: textureVertex;
                vertex: vertex].
    OpenGLLibrary endPolygon!
   
vertices
    ^vertices!

vertices: verticesCollection
    vertices := verticesCollection! !

