From 75a7e5ff20dbc3f3b583d31b4a1ff0b6a1459d49 Mon Sep 17 00:00:00 2001 From: andy <unknown> Date: Tue, 3 Oct 2000 18:25:29 +0000 Subject: [PATCH] [project @ 2000-10-03 18:25:28 by andy] Addding the Galois Connections ray tracer as an example for GHC. --- ghc/tests/programs/galois_raytrace/CSG.hs | 16 + .../programs/galois_raytrace/Construct.hs | 265 ++++++++++++ ghc/tests/programs/galois_raytrace/Data.hs | 407 ++++++++++++++++++ ghc/tests/programs/galois_raytrace/Eval.hs | 357 +++++++++++++++ .../programs/galois_raytrace/Geometry.hs | 314 ++++++++++++++ .../programs/galois_raytrace/Illumination.hs | 212 +++++++++ .../programs/galois_raytrace/Intersections.hs | 404 +++++++++++++++++ .../programs/galois_raytrace/Interval.hs | 121 ++++++ ghc/tests/programs/galois_raytrace/Main.hs | 17 + ghc/tests/programs/galois_raytrace/Makefile | 9 + ghc/tests/programs/galois_raytrace/Misc.hs | 11 + ghc/tests/programs/galois_raytrace/Parse.hs | 137 ++++++ ghc/tests/programs/galois_raytrace/Pixmap.hs | 64 +++ .../programs/galois_raytrace/Primitives.hs | 24 ++ .../programs/galois_raytrace/RayTrace.hs | 9 + ghc/tests/programs/galois_raytrace/Surface.hs | 115 +++++ ghc/tests/programs/galois_raytrace/galois.gml | 147 +++++++ .../galois_raytrace/galois_raytrace.stdout | Bin 0 -> 180024 bytes 18 files changed, 2629 insertions(+) create mode 100644 ghc/tests/programs/galois_raytrace/CSG.hs create mode 100644 ghc/tests/programs/galois_raytrace/Construct.hs create mode 100644 ghc/tests/programs/galois_raytrace/Data.hs create mode 100644 ghc/tests/programs/galois_raytrace/Eval.hs create mode 100644 ghc/tests/programs/galois_raytrace/Geometry.hs create mode 100644 ghc/tests/programs/galois_raytrace/Illumination.hs create mode 100644 ghc/tests/programs/galois_raytrace/Intersections.hs create mode 100644 ghc/tests/programs/galois_raytrace/Interval.hs create mode 100644 ghc/tests/programs/galois_raytrace/Main.hs create mode 100644 ghc/tests/programs/galois_raytrace/Makefile create mode 100644 ghc/tests/programs/galois_raytrace/Misc.hs create mode 100644 ghc/tests/programs/galois_raytrace/Parse.hs create mode 100644 ghc/tests/programs/galois_raytrace/Pixmap.hs create mode 100644 ghc/tests/programs/galois_raytrace/Primitives.hs create mode 100644 ghc/tests/programs/galois_raytrace/RayTrace.hs create mode 100644 ghc/tests/programs/galois_raytrace/Surface.hs create mode 100644 ghc/tests/programs/galois_raytrace/galois.gml create mode 100644 ghc/tests/programs/galois_raytrace/galois_raytrace.stdout diff --git a/ghc/tests/programs/galois_raytrace/CSG.hs b/ghc/tests/programs/galois_raytrace/CSG.hs new file mode 100644 index 000000000000..ba37a17b25e7 --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/CSG.hs @@ -0,0 +1,16 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module CSG(module Construct, + module Geometry, + module Intersections, + module Interval, + module Misc) where + +import Construct +import Geometry +import Intersections +import Interval +import Misc diff --git a/ghc/tests/programs/galois_raytrace/Construct.hs b/ghc/tests/programs/galois_raytrace/Construct.hs new file mode 100644 index 000000000000..90dbc60f9e51 --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Construct.hs @@ -0,0 +1,265 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Construct + ( Surface (..) + , Face (..) + , CSG (..) + , Texture + , Transform + , union, intersect, difference + , plane, sphere, cube, cylinder, cone + , transform + , translate, translateX, translateY, translateZ + , scale, scaleX, scaleY, scaleZ, uscale + , rotateX, rotateY, rotateZ + , eye, translateEye + , rotateEyeX, rotateEyeY, rotateEyeZ + ) where + +import Geometry + +-- In each case, we model the surface by a point and a pair of tangent vectors. +-- This gives us enough information to determine the surface +-- normal at that point, which is all that is required by the current +-- illumination model. We can't just save the surface normal because +-- that isn't preserved by transformations. + +data Surface + = Planar Point Vector Vector + | Spherical Point Vector Vector + | Cylindrical Point Vector Vector + | Conic Point Vector Vector + deriving Show + +data Face + = PlaneFace + | SphereFace + | CubeFront + | CubeBack + | CubeLeft + | CubeRight + | CubeTop + | CubeBottom + | CylinderSide + | CylinderTop + | CylinderBottom + | ConeSide + | ConeBase + deriving Show + +data CSG a + = Plane a + | Sphere a + | Cylinder a + | Cube a + | Cone a + | Transform Matrix Matrix (CSG a) + | Union (CSG a) (CSG a) + | Intersect (CSG a) (CSG a) + | Difference (CSG a) (CSG a) + | Box Box (CSG a) + deriving (Show) + +-- the data returned for determining surface texture +-- the Face tells which face of a primitive this is +-- the Point is the point of intersection in object coordinates +-- the a is application-specific texture information +type Texture a = (Face, Point, a) + +union, intersect, difference :: CSG a -> CSG a -> CSG a + +union p@(Box b1 _) q@(Box b2 _) = Box (mergeBox b1 b2) (Union p q) +union p q = Union p q + +-- rather pessimistic +intersect p@(Box b1 _) q@(Box b2 _) = Box (mergeBox b1 b2) (Intersect p q) +intersect p q = Intersect p q + +difference (Box b1 p) q = Box b1 (Difference p q) +-- no need to box again inside +-- difference p@(Box b1 _) q = Box b1 (Difference p q) +difference p q = Difference p q + +mkBox b p = Box b p + +plane, sphere, cube, cylinder, cone :: a -> CSG a + +plane = Plane +sphere s = + mkBox (B (-1 - epsilon) (1 + epsilon) + (-1 - epsilon) (1 + epsilon) + (-1 - epsilon) (1 + epsilon)) (Sphere s) +cone s = + mkBox (B (-1 - epsilon) (1 + epsilon) + ( - epsilon) (1 + epsilon) + (-1 - epsilon) (1 + epsilon)) (Cone s) +cube s = + mkBox (B (- epsilon) (1 + epsilon) + (- epsilon) (1 + epsilon) + (- epsilon) (1 + epsilon)) (Cube s) +cylinder s = + mkBox (B (-1 - epsilon) (1 + epsilon) + ( - epsilon) (1 + epsilon) + (-1 - epsilon) (1 + epsilon)) (Cylinder s) + +---------------------------- +-- Object transformations +---------------------------- + +type Transform = (Matrix, Matrix) + +transform :: Transform -> CSG a -> CSG a + +transform (m, m') (Transform mp mp' p) = Transform (multMM m mp) (multMM mp' m') p +transform mm' (Union p q) = Union (transform mm' p) (transform mm' q) +transform mm' (Intersect p q) = Intersect (transform mm' p) (transform mm' q) +transform mm' (Difference p q) = Difference (transform mm' p) (transform mm' q) +transform mm'@(m,_) (Box box p) = Box (transformBox m box) (transform mm' p) +transform (m, m') prim = Transform m m' prim + +translate :: Coords -> CSG a -> CSG a +translateX, translateY, translateZ :: Double -> CSG a -> CSG a + +translate xyz = transform $ transM xyz +translateX x = translate (x, 0, 0) +translateY y = translate (0, y, 0) +translateZ z = translate (0, 0, z) + +scale :: Coords -> CSG a -> CSG a +scaleX, scaleY, scaleZ, uscale :: Double -> CSG a -> CSG a + +scale xyz = transform $ scaleM xyz +scaleX x = scale (x, 1, 1) +scaleY y = scale (1, y, 1) +scaleZ z = scale (1, 1, z) +uscale u = scale (u,u,u) + +rotateX, rotateY, rotateZ :: Radian -> CSG a -> CSG a + +rotateX a = transform $ rotxM a +rotateY a = transform $ rotyM a +rotateZ a = transform $ rotzM a + +unit = matrix + ( ( 1.0, 0.0, 0.0, 0.0 ), + ( 0.0, 1.0, 0.0, 0.0 ), + ( 0.0, 0.0, 1.0, 0.0 ), + ( 0.0, 0.0, 0.0, 1.0 ) ) + +transM (x, y, z) + = ( matrix + ( ( 1, 0, 0, x ), + ( 0, 1, 0, y ), + ( 0, 0, 1, z ), + ( 0, 0, 0, 1 ) ), + matrix + ( ( 1, 0, 0, -x ), + ( 0, 1, 0, -y ), + ( 0, 0, 1, -z ), + ( 0, 0, 0, 1 ) ) ) + +scaleM (x, y, z) + = ( matrix + ( ( x', 0, 0, 0 ), + ( 0, y', 0, 0 ), + ( 0, 0, z', 0 ), + ( 0, 0, 0, 1 ) ), + matrix + ( ( 1/x', 0, 0, 0 ), + ( 0, 1/y', 0, 0 ), + ( 0, 0, 1/z', 0 ), + ( 0, 0, 0, 1 ) ) ) + where x' = nonZero x + y' = nonZero y + z' = nonZero z + +rotxM t + = ( matrix + ( ( 1, 0, 0, 0 ), + ( 0, cos t, -sin t, 0 ), + ( 0, sin t, cos t, 0 ), + ( 0, 0, 0, 1 ) ), + matrix + ( ( 1, 0, 0, 0 ), + ( 0, cos t, sin t, 0 ), + ( 0, -sin t, cos t, 0 ), + ( 0, 0, 0, 1 ) ) ) + +rotyM t + = ( matrix + ( ( cos t, 0, sin t, 0 ), + ( 0, 1, 0, 0 ), + ( -sin t, 0, cos t, 0 ), + ( 0, 0, 0, 1 ) ), + matrix + ( ( cos t, 0, -sin t, 0 ), + ( 0, 1, 0, 0 ), + ( sin t, 0, cos t, 0 ), + ( 0, 0, 0, 1 ) ) ) + +rotzM t + = ( matrix + ( ( cos t, -sin t, 0, 0 ), + ( sin t, cos t, 0, 0 ), + ( 0, 0, 1, 0 ), + ( 0, 0, 0, 1 ) ), + matrix + ( ( cos t, sin t, 0, 0 ), + ( -sin t, cos t, 0, 0 ), + ( 0, 0, 1, 0 ), + ( 0, 0, 0, 1 ) ) ) + +------------------- +-- Eye transformations + +-- These are used to specify placement of the eye. +-- `eye' starts out at (0, 0, -1). +-- These are implemented as inverse transforms of the model. +------------------- + +eye :: Transform +translateEye :: Coords -> Transform -> Transform +rotateEyeX, rotateEyeY, rotateEyeZ :: Radian -> Transform -> Transform + +eye = (unit, unit) +translateEye xyz (eye1, eye2) + = (multMM m1 eye1, multMM eye2 m2) + where (m1, m2) = transM xyz +rotateEyeX t (eye1, eye2) + = (multMM m1 eye1, multMM eye2 m2) + where (m1, m2) = rotxM t +rotateEyeY t (eye1, eye2) + = (multMM m1 eye1, multMM eye2 m2) + where (m1, m2) = rotyM t +rotateEyeZ t (eye1, eye2) + = (multMM m1 eye1, multMM eye2 m2) + where (m1, m2) = rotzM t + +------------------- +-- Bounding boxes +------------------- + +mergeBox (B x11 x12 y11 y12 z11 z12) (B x21 x22 y21 y22 z21 z22) = + B (x11 `min` x21) (x12 `max` x22) + (y11 `min` y21) (y12 `max` y22) + (z11 `min` z21) (z12 `max` z22) + +transformBox t (B x1 x2 y1 y2 z1 z2) + = (B (foldr1 min (map xCoord pts')) + (foldr1 max (map xCoord pts')) + (foldr1 min (map yCoord pts')) + (foldr1 max (map yCoord pts')) + (foldr1 min (map zCoord pts')) + (foldr1 max (map zCoord pts'))) + where pts' = map (multMP t) pts + pts = [point x1 y1 z1, + point x1 y1 z2, + point x1 y2 z1, + point x1 y2 z2, + point x2 y1 z1, + point x2 y1 z2, + point x2 y2 z1, + point x2 y2 z2] diff --git a/ghc/tests/programs/galois_raytrace/Data.hs b/ghc/tests/programs/galois_raytrace/Data.hs new file mode 100644 index 000000000000..1f716ea5e0cb --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Data.hs @@ -0,0 +1,407 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Data where + +import Array +import IOExts + +import CSG +import Geometry +import Illumination +import Primitives +import Surface + +-- Now the parsed (expresssion) language + +type Name = String + +type Code = [GMLToken] + +data GMLToken + -- All these can occur in parsed code + = TOp GMLOp + | TId Name + | TBind Name + | TBool Bool + | TInt Int + | TReal Double + | TString String + | TBody Code + | TArray Code + | TApply + | TIf + -- These can occur in optimized/transformed code + -- NONE (yet!) + + +instance Show GMLToken where + showsPrec p (TOp op) = shows op + showsPrec p (TId id) = showString id + showsPrec p (TBind id) = showString ('/' : id) + showsPrec p (TBool bool) = shows bool + showsPrec p (TInt i) = shows i + showsPrec p (TReal d) = shows d + showsPrec p (TString s) = shows s + showsPrec p (TBody code) = shows code + showsPrec p (TArray code) = showString "[ " + . foldr (\ a b -> a . showChar ' ' . b) id (map shows code) + . showString "]" + showsPrec p (TApply) = showString "apply" + showsPrec p (TIf) = showString "if" + + showList code = showString "{ " + . foldr (\ a b -> a . showChar ' ' . b) id (map shows code) + . showString "}" + + +-- Now the value language, used inside the interpreter + +type Stack = [GMLValue] + +data GMLValue + = VBool !Bool + | VInt !Int + | VReal !Double + | VString String + | VClosure Env Code + | VArray (Array Int GMLValue) -- FIXME: Haskell array + -- uses the interpreter version of point + | VPoint { xPoint :: !Double + , yPoint :: !Double + , zPoint :: !Double + } + -- these are abstract to the interpreter + | VObject Object + | VLight Light + -- This is an abstract object, used by the abstract interpreter + | VAbsObj AbsObj + + +-- There are only *3* basic abstract values, +-- and the combinators also. + +data AbsObj + = AbsFACE + | AbsU + | AbsV + deriving (Show) + +instance Show GMLValue where + showsPrec p value = showString (showStkEle value) + +showStkEle :: GMLValue -> String +showStkEle (VBool b) = show b ++ " :: Bool" +showStkEle (VInt i) = show i ++ " :: Int" +showStkEle (VReal r) = show r ++ " :: Real" +showStkEle (VString s) = show s ++ " :: String" +showStkEle (VClosure {}) = "<closure> :: Closure" +showStkEle (VArray arr) + = "<array (" ++ show (succ (snd (bounds arr))) ++ " elements)> :: Array" +showStkEle (VPoint x y z) = "(" ++ show x + ++ "," ++ show y + ++ "," ++ show z + ++ ") :: Point" +showStkEle (VObject {}) = "<Object> :: Object" +showStkEle (VLight {}) = "<Light> :: Object" +showStkEle (VAbsObj vobs) = "{{ " ++ show vobs ++ "}} :: AbsObj" + +-- An abstract environment + +newtype Env = Env [(Name, GMLValue)] deriving Show + +emptyEnv :: Env +emptyEnv = Env [] + +extendEnv :: Env -> Name -> GMLValue -> Env +extendEnv (Env e) n v = Env ((n, v):e) + +lookupEnv :: Env -> Name -> Maybe GMLValue +lookupEnv (Env e) n = lookup n e + +-- All primitive operators +-- +-- There is no Op_apply, Op_false, Op_true and Op_if +-- (because they appear explcitly in the rules). + +data GMLOp + = Op_acos + | Op_addi + | Op_addf + | Op_asin + | Op_clampf + | Op_cone + | Op_cos + | Op_cube + | Op_cylinder + | Op_difference + | Op_divi + | Op_divf + | Op_eqi + | Op_eqf + | Op_floor + | Op_frac + | Op_get + | Op_getx + | Op_gety + | Op_getz + | Op_intersect + | Op_length + | Op_lessi + | Op_lessf + | Op_light + | Op_modi + | Op_muli + | Op_mulf + | Op_negi + | Op_negf + | Op_plane + | Op_point + | Op_pointlight + | Op_real + | Op_render + | Op_rotatex + | Op_rotatey + | Op_rotatez + | Op_scale + | Op_sin + | Op_sphere + | Op_spotlight + | Op_sqrt + | Op_subi + | Op_subf + | Op_trace -- non standard, for debugging GML programs + | Op_translate + | Op_union + | Op_uscale + deriving (Eq,Ord,Ix,Bounded) + +instance Show GMLOp where + showsPrec _ op = showString (opNameTable ! op) + + +------------------------------------------------------------------------------ + +-- And how we use the op codes (there names, there interface) + +-- These keywords include, "apply", "if", "true" and "false", +-- they are not parsed as operators, but are +-- captured by the parser as a special case. + +keyWords :: [String] +keyWords = [ kwd | (kwd,_,_) <- opcodes ] + +-- Lookup has to look from the start (or else...) +opTable :: [(Name,GMLToken)] +opTable = [ (kwd,op) | (kwd,op,_) <- opcodes ] + +opNameTable :: Array GMLOp Name +opNameTable = array (minBound,maxBound) + [ (op,name) | (name,TOp op,_) <- opcodes ] + +undef = error "undefined function" +image = error "undefined function: talk to image group" + +-- typically, its best to have *one* opcode table, +-- so that mis-alignments do not happen. + +opcodes :: [(String,GMLToken,PrimOp)] +opcodes = + [ ("apply", TApply, error "incorrect use of apply") + , ("if", TIf, error "incorrect use of if") + , ("false", TBool False, error "incorrect use of false") + , ("true", TBool True, error "incorrect use of true") + ] ++ map (\ (a,b,c) -> (a,TOp b,c)) + -- These are just invocation, any coersions need to occur between here + -- and before arriving at the application code (like deg -> rad). + [ ("acos", Op_acos, Real_Real (rad2deg . acos)) + , ("addi", Op_addi, Int_Int_Int (+)) + , ("addf", Op_addf, Real_Real_Real (+)) + , ("asin", Op_asin, Real_Real (rad2deg . asin)) + , ("clampf", Op_clampf, Real_Real clampf) + , ("cone", Op_cone, Surface_Obj cone) + , ("cos", Op_cos, Real_Real (cos . deg2rad)) + , ("cube", Op_cube, Surface_Obj cube) + , ("cylinder", Op_cylinder, Surface_Obj cylinder) + , ("difference", Op_difference, Obj_Obj_Obj difference) + , ("divi", Op_divi, Int_Int_Int (ourQuot)) + , ("divf", Op_divf, Real_Real_Real (/)) + , ("eqi", Op_eqi, Int_Int_Bool (==)) + , ("eqf", Op_eqf, Real_Real_Bool (==)) + , ("floor", Op_floor, Real_Int floor) + , ("frac", Op_frac, Real_Real (snd . properFraction)) + , ("get", Op_get, Arr_Int_Value ixGet) + , ("getx", Op_getx, Point_Real (\ x y z -> x)) + , ("gety", Op_gety, Point_Real (\ x y z -> y)) + , ("getz", Op_getz, Point_Real (\ x y z -> z)) + , ("intersect", Op_intersect, Obj_Obj_Obj intersect) + , ("length", Op_length, Arr_Int (succ . snd . bounds)) + , ("lessi", Op_lessi, Int_Int_Bool (<)) + , ("lessf", Op_lessf, Real_Real_Bool (<)) + , ("light", Op_light, Point_Color_Light light) + , ("modi", Op_modi, Int_Int_Int (ourRem)) + , ("muli", Op_muli, Int_Int_Int (*)) + , ("mulf", Op_mulf, Real_Real_Real (*)) + , ("negi", Op_negi, Int_Int negate) + , ("negf", Op_negf, Real_Real negate) + , ("plane", Op_plane, Surface_Obj plane) + , ("point", Op_point, Real_Real_Real_Point VPoint) + , ("pointlight", Op_pointlight, Point_Color_Light pointlight) + , ("real", Op_real, Int_Real fromIntegral) + , ("render", Op_render, Render $ render eye) + , ("rotatex", Op_rotatex, Obj_Real_Obj (\ o d -> rotateX (deg2rad d) o)) + , ("rotatey", Op_rotatey, Obj_Real_Obj (\ o d -> rotateY (deg2rad d) o)) + , ("rotatez", Op_rotatez, Obj_Real_Obj (\ o d -> rotateZ (deg2rad d) o)) + , ("scale", Op_scale, Obj_Real_Real_Real_Obj (\ o x y z -> scale (x,y,z) o)) + , ("sin", Op_sin, Real_Real (sin . deg2rad)) + , ("sphere", Op_sphere, Surface_Obj sphere') -- see comment at end of file + , ("spotlight", Op_spotlight, Point_Point_Color_Real_Real_Light mySpotlight) + , ("sqrt", Op_sqrt, Real_Real ourSqrt) + , ("subi", Op_subi, Int_Int_Int (-)) + , ("subf", Op_subf, Real_Real_Real (-)) + , ("trace", Op_trace, Value_String_Value mytrace) + , ("translate", Op_translate, Obj_Real_Real_Real_Obj (\ o x y z -> translate (x,y,z) o)) + , ("union", Op_union, Obj_Obj_Obj union) + , ("uscale", Op_uscale, Obj_Real_Obj (\ o r -> uscale r o)) + ] + +-- This enumerate all possible ways of calling the fixed primitives + +-- The datatype captures the type at the *interp* level, +-- the type of the functional is mirrored on this (using Haskell types). + +data PrimOp + + -- 1 argument + = Int_Int (Int -> Int) + | Real_Real (Double -> Double) + | Point_Real (Double -> Double -> Double -> Double) + | Surface_Obj (SurfaceFn Color Double -> Object) + | Real_Int (Double -> Int) + | Int_Real (Int -> Double) + | Arr_Int (Array Int GMLValue -> Int) + + -- 2 arguments + | Int_Int_Int (Int -> Int -> Int) + | Int_Int_Bool (Int -> Int -> Bool) + | Real_Real_Real (Double -> Double -> Double) + | Real_Real_Bool (Double -> Double -> Bool) + | Arr_Int_Value (Array Int GMLValue -> Int -> GMLValue) + + -- Many arguments, typically image mangling + + | Obj_Obj_Obj (Object -> Object -> Object) + | Point_Color_Light (Coords -> Color -> Light) + | Real_Real_Real_Point (Double -> Double -> Double -> GMLValue) + | Obj_Real_Obj (Object -> Double -> Object) + | Obj_Real_Real_Real_Obj (Object -> Double -> Double -> Double -> Object) + | Value_String_Value (GMLValue -> String -> GMLValue) + + | Point_Point_Color_Real_Real_Light + (Coords -> Coords -> Color -> Radian -> Radian -> Light) + -- And finally render + | Render (Color -> [Light] -> Object -> Int -> Double -> Int -> Int -> String -> IO ()) + +data Type + = TyBool + | TyInt + | TyReal + | TyString + | TyCode + | TyArray + | TyPoint + | TyObject + | TyLight + | TyAlpha + | TyAbsObj + deriving (Eq,Ord,Ix,Bounded) + +typeTable = + [ ( TyBool, "Bool") + , ( TyInt, "Int") + , ( TyReal, "Real") + , ( TyString, "String") + , ( TyCode, "Code") + , ( TyArray, "Array") + , ( TyPoint, "Point") + , ( TyObject, "Object") + , ( TyLight, "Light") + , ( TyAlpha, "<anything>") + , ( TyAbsObj, "<abs>") + ] + +typeNames = array (minBound,maxBound) typeTable + +instance Show Type where + showsPrec _ op = showString (typeNames ! op) + +getPrimOpType :: PrimOp -> [Type] +getPrimOpType (Int_Int _) = [TyInt] +getPrimOpType (Real_Real _) = [TyReal] +getPrimOpType (Point_Real _) = [TyPoint] +getPrimOpType (Surface_Obj _) = [TyCode] +getPrimOpType (Real_Int _) = [TyReal] +getPrimOpType (Int_Real _) = [TyInt] +getPrimOpType (Arr_Int _) = [TyArray] +getPrimOpType (Int_Int_Int _) = [TyInt,TyInt] +getPrimOpType (Int_Int_Bool _) = [TyInt,TyInt] +getPrimOpType (Real_Real_Real _) = [TyReal,TyReal] +getPrimOpType (Real_Real_Bool _) = [TyReal,TyReal] +getPrimOpType (Arr_Int_Value _) = [TyArray,TyInt] +getPrimOpType (Obj_Obj_Obj _) = [TyObject,TyObject] +getPrimOpType (Point_Color_Light _) = [TyPoint,TyPoint] +getPrimOpType (Real_Real_Real_Point _) = [TyReal,TyReal,TyReal] +getPrimOpType (Obj_Real_Obj _) = [TyObject,TyReal] +getPrimOpType (Obj_Real_Real_Real_Obj _) = [TyObject,TyReal,TyReal,TyReal] +getPrimOpType (Value_String_Value _) = [TyAlpha,TyString] +getPrimOpType (Point_Point_Color_Real_Real_Light _) + = [TyPoint,TyPoint,TyPoint,TyReal,TyReal] +getPrimOpType (Render _) = [TyPoint, + TyLight, + TyObject, + TyInt, + TyReal, + TyReal, + TyReal, + TyString] + + +-- Some primitives with better error message + +mytrace v s = trace (s ++" : "++ show v ++ "\n") v + + +ixGet :: Array Int GMLValue -> Int -> GMLValue +ixGet arr i + | inRange (bounds arr) i = arr ! i + | otherwise = error ("failed access with index value " + ++ show i + ++ " (should be between 0 and " + ++ show (snd (bounds arr)) ++ ")") + +ourQuot :: Int -> Int -> Int +ourQuot _ 0 = error "attempt to use divi to divide by 0" +ourQuot a b = a `quot` b + +ourRem :: Int -> Int -> Int +ourRem _ 0 = error "attempt to use remi to divide by 0" +ourRem a b = a `rem` b + +ourSqrt :: Double -> Double +ourSqrt n | n < 0 = error "attempt to use sqrt on a negative number" + | otherwise = sqrt n + + +mySpotlight p1 p2 col cutoff exp = spotlight p1 p2 col (deg2rad cutoff) exp + +-- The problem specification gets the mapping for spheres backwards +-- (it maps the image from right to left). +-- We've fixed that in the raytracing library so that it goes from left +-- to right, but to keep the GML front compatible with the problem +-- statement, we reverse it here. + +sphere' :: SurfaceFn Color Double -> CSG (SurfaceFn Color Double) +sphere' (SFun f) = sphere (SFun (\i u v -> f i (1 - u) v)) +sphere' s = sphere s diff --git a/ghc/tests/programs/galois_raytrace/Eval.hs b/ghc/tests/programs/galois_raytrace/Eval.hs new file mode 100644 index 000000000000..9d00cd9b67bd --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Eval.hs @@ -0,0 +1,357 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Eval where + +import Array + +import IOExts + +import Geometry +import CSG +import Surface +import Data +import Parse (rayParse, rayParseF) + +class Monad m => MonadEval m where + doOp :: PrimOp -> GMLOp -> Stack -> m Stack + tick :: m () + err :: String -> m a + + tick = return () + +newtype Pure a = Pure a deriving Show + +instance Monad Pure where + Pure x >>= k = k x + return = Pure + fail s = error s + +instance MonadEval Pure where + doOp = doPureOp + err s = error s + +instance MonadEval IO where + doOp prim op stk = do { -- putStrLn ("Calling " ++ show op + -- ++ " << " ++ show stk ++ " >>") + doAllOp prim op stk + } + err s = error s + +data State + = State { env :: Env + , stack :: Stack + , code :: Code + } deriving Show + +callback :: Env -> Code -> Stack -> Stack +callback env code stk + = case eval (State { env = env, stack = stk, code = code}) of + Pure stk -> stk + +{-# SPECIALIZE eval :: State -> Pure Stack #-} +{-# SPECIALIZE eval :: State -> IO Stack #-} + +eval :: MonadEval m => State -> m Stack +eval st = + do { () <- return () -- $ unsafePerformIO (print st) -- Functional debugger + ; if moreCode st then + do { tick -- tick first, so as to catch loops on new eval. + ; st' <- step st + ; eval st' + } + else return (stack st) + } + +moreCode :: State -> Bool +moreCode (State {code = []}) = False +moreCode _ = True + +-- Step has a precondition that there *is* code to run +{-# SPECIALIZE step :: State -> Pure State #-} +{-# SPECIALIZE step :: State -> IO State #-} +step :: MonadEval m => State -> m State + +-- Rule 1: Pushing BaseValues +step st@(State{ stack = stack, code = (TBool b):cs }) + = return (st { stack = (VBool b):stack, code = cs }) +step st@(State{ stack = stack, code = (TInt i):cs }) + = return (st { stack = (VInt i):stack, code = cs }) +step st@(State{ stack = stack, code = (TReal r):cs }) + = return (st { stack = (VReal r):stack, code = cs }) +step st@(State{ stack = stack, code = (TString s):cs }) + = return (st { stack = (VString s):stack, code = cs }) + +-- Rule 2: Name binding +step st@(State{ env = env, stack = (v:stack), code = (TBind id):cs }) = + return (State { env = extendEnv env id v, stack = stack, code = cs }) +step st@(State{ env = env, stack = [], code = (TBind id):cs }) = + err "Attempt to bind the top of an empty stack" + +-- Rule 3: Name lookup +step st@(State{ env = env, stack = stack, code = (TId id):cs }) = + case (lookupEnv env id) of + Just v -> return (st { stack = v:stack, code = cs }) + Nothing -> err ("Cannot find value for identifier: " ++ id) + +-- Rule 4: Closure creation +step st@(State{ env = env, stack = stack, code = (TBody body):cs }) = + return (st { stack = (VClosure env body):stack, code = cs }) + +-- Rule 5: Application +step st@(State{ env = env, stack = (VClosure env' code'):stack, code = TApply:cs }) = + do { stk <- eval (State {env = env', stack = stack, code = code'}) + ; return (st { stack = stk, code = cs }) + } +step st@(State{ env = env, stack = [], code = TApply:cs }) = + err "Application with an empty stack" +step st@(State{ env = env, stack = _:_, code = TApply:cs }) = + err "Application of a non-closure" + +-- Rule 6: Arrays +step st@(State{ env = env, stack = stack, code = TArray code':cs }) = + do { stk <- eval (State {env = env, stack = [], code = code'}) + ; let last = length stk-1 + ; let arr = array (0,last) (zip [last,last-1..] stk) + ; return (st { stack = (VArray arr):stack, code = cs }) + } + +-- Rule 7 & 8: If statement +step st@(State{ env = env, stack = (VClosure e2 c2):(VClosure e1 c1):(VBool True):stack, code = TIf:cs }) = + do { stk <- eval (State {env = e1, stack = stack, code = c1}) + ; return (st { stack = stk, code = cs }) + } +step st@(State{ env = env, stack = (VClosure e2 c2):(VClosure e1 c1):(VBool False):stack, code = TIf:cs }) = + do { stk <- eval (State {env = e2, stack = stack, code = c2}) + ; return (st { stack = stk, code = cs }) + } +step st@(State{ env = env, stack = _, code = TIf:cs }) = + err "Incorrect use of if (bad and/or inappropriate values on the stack)" + +-- Rule 9: Operators +step st@(State{ env = env, stack = stack, code = (TOp op):cs }) = + do { stk <- doOp (opFnTable ! op) op stack + ; return (st { stack = stk, code = cs }) + } + +-- Rule Opps +step _ = err "Tripped on sidewalk while stepping." + + +-------------------------------------------------------------------------- +-- Operator code + +opFnTable :: Array GMLOp PrimOp +opFnTable = array (minBound,maxBound) + [ (op,prim) | (_,TOp op,prim) <- opcodes ] + + + + +doPureOp :: (MonadEval m) => PrimOp -> GMLOp -> Stack -> m Stack +doPureOp _ Op_render _ = + err ("\nAttempting to call render from inside a purely functional callback.") +doPureOp primOp op stk = doPrimOp primOp op stk -- call the purely functional operators + +{-# SPECIALIZE doPrimOp :: PrimOp -> GMLOp -> Stack -> Pure Stack #-} +{-# SPECIALIZE doPrimOp :: PrimOp -> GMLOp -> Stack -> IO Stack #-} +{-# SPECIALIZE doPrimOp :: PrimOp -> GMLOp -> Stack -> Abs Stack #-} + +doPrimOp :: (MonadEval m) => PrimOp -> GMLOp -> Stack -> m Stack + +-- 1 argument. + +doPrimOp (Int_Int fn) _ (VInt i1:stk) + = return ((VInt (fn i1)) : stk) +doPrimOp (Real_Real fn) _ (VReal r1:stk) + = return ((VReal (fn r1)) : stk) +doPrimOp (Point_Real fn) _ (VPoint x y z:stk) + = return ((VReal (fn x y z)) : stk) + +-- This is where the callbacks happen from... +doPrimOp (Surface_Obj fn) _ (VClosure env code:stk) + = case absapply env code [VAbsObj AbsFACE,VAbsObj AbsU,VAbsObj AbsV] of + Just [VReal r3,VReal r2,VReal r1,VPoint c1 c2 c3] -> + let + res = prop (color c1 c2 c3) r1 r2 r3 + in + return ((VObject (fn (SConst res))) : stk) + _ -> return ((VObject (fn (SFun call))) : stk) + where + -- The most general case + call i r1 r2 = + case callback env code [VReal r2,VReal r1,VInt i] of + [VReal r3,VReal r2,VReal r1,VPoint c1 c2 c3] + -> prop (color c1 c2 c3) r1 r2 r3 + stk -> error ("callback failed: incorrectly typed return arguments" + ++ show stk) + +doPrimOp (Real_Int fn) _ (VReal r1:stk) + = return ((VInt (fn r1)) : stk) +doPrimOp (Int_Real fn) _ (VInt r1:stk) + = return ((VReal (fn r1)) : stk) +doPrimOp (Arr_Int fn) _ (VArray arr:stk) + = return ((VInt (fn arr)) : stk) + +-- 2 arguments. + +doPrimOp (Int_Int_Int fn) _ (VInt i2:VInt i1:stk) + = return ((VInt (fn i1 i2)) : stk) +doPrimOp (Int_Int_Bool fn) _ (VInt i2:VInt i1:stk) + = return ((VBool (fn i1 i2)) : stk) +doPrimOp (Real_Real_Real fn) _ (VReal r2:VReal r1:stk) + = return ((VReal (fn r1 r2)) : stk) +doPrimOp (Real_Real_Bool fn) _ (VReal r2:VReal r1:stk) + = return ((VBool (fn r1 r2)) : stk) +doPrimOp (Arr_Int_Value fn) _ (VInt i:VArray arr:stk) + = return ((fn arr i) : stk) + + + -- Many arguments, typically image mangling + +doPrimOp (Obj_Obj_Obj fn) _ (VObject o2:VObject o1:stk) + = return ((VObject (fn o1 o2)) : stk) +doPrimOp (Point_Color_Light fn) _ (VPoint r g b:VPoint x y z : stk) + = return (VLight (fn (x,y,z) (color r g b)) : stk) +doPrimOp (Point_Point_Color_Real_Real_Light fn) _ + (VReal r2:VReal r1:VPoint r g b:VPoint x2 y2 z2:VPoint x1 y1 z1 : stk) + = return (VLight (fn (x1,y1,z1) (x2,y2,z2) (color r g b) r1 r2) : stk) +doPrimOp (Real_Real_Real_Point fn) _ (VReal r3:VReal r2:VReal r1:stk) + = return ((fn r1 r2 r3) : stk) +doPrimOp (Obj_Real_Obj fn) _ (VReal r:VObject o:stk) + = return (VObject (fn o r) : stk) +doPrimOp (Obj_Real_Real_Real_Obj fn) _ (VReal r3:VReal r2:VReal r1:VObject o:stk) + = return (VObject (fn o r1 r2 r3) : stk) + +-- This one is our testing harness +doPrimOp (Value_String_Value fn) _ (VString s:o:stk) + = res `seq` return (res : stk) + where + res = fn o s + +doPrimOp primOp op args + = err ("\n\ntype error when attempting to execute builtin primitive \"" ++ + show op ++ "\"\n\n| " ++ + show op ++ " takes " ++ show (length types) ++ " argument" ++ s + ++ " with" ++ the ++ " type" ++ s ++ "\n|\n|" ++ + " " ++ unwords [ show ty | ty <- types ] ++ "\n|\n|" ++ + " currently, the relevent argument" ++ s ++ " on the stack " ++ + are ++ "\n|\n| " ++ + unwords [ "(" ++ show arg ++ ")" + | arg <- reverse (take (length types) args) ] ++ "\n|\n| " + ++ " (top of stack is on the right hand side)\n\n") + where + len = length types + s = (if len /= 1 then "s" else "") + are = (if len /= 1 then "are" else "is") + the = (if len /= 1 then "" else " the") + types = getPrimOpType primOp + + +-- Render is somewhat funny, becauase it can only get called at top level. +-- All other operations are purely functional. + +doAllOp :: PrimOp -> GMLOp -> Stack -> IO Stack +doAllOp (Render render) Op_render + (VString str:VInt ht:VInt wid:VReal fov + :VInt dep:VObject obj:VArray arr + :VPoint r g b : stk) + = do { render (color r g b) lights obj dep (fov * (pi / 180.0)) wid ht str + ; return stk + } + where + lights = [ light | (VLight light) <- elems arr ] + +doAllOp primOp op stk = doPrimOp primOp op stk -- call the purely functional operators + +------------------------------------------------------------------------------ +{- + - Abstract evaluation. + - + - The idea is you check for constant code that + - (1) does not look at its arguments + - (2) gives a fixed result + - + - We run for 100 steps. + - + -} + +absapply :: Env -> Code -> Stack -> Maybe Stack +absapply env code stk = + case runAbs (eval (State env stk code)) 100 of + AbsState stk _ -> Just stk + AbsFail m -> Nothing + +newtype Abs a = Abs { runAbs :: Int -> AbsState a } +data AbsState a = AbsState a !Int + | AbsFail String + +instance Monad Abs where + (Abs fn) >>= k = Abs (\ s -> case fn s of + AbsState r s' -> runAbs (k r) s' + AbsFail m -> AbsFail m) + return x = Abs (\ n -> AbsState x n) + fail s = Abs (\ n -> AbsFail s) + +instance MonadEval Abs where + doOp = doAbsOp + err = fail + tick = Abs (\ n -> if n <= 0 + then AbsFail "run out of time" + else AbsState () (n-1)) + +doAbsOp :: PrimOp -> GMLOp -> Stack -> Abs Stack +doAbsOp _ Op_point (VReal r3:VReal r2:VReal r1:stk) + = return ((VPoint r1 r2 r3) : stk) + -- here, you could have an (AbsPoint :: AbsObj) which you put on the + -- stack, with any object in the three fields. +doAbsOp _ op _ = err ("operator not understood (" ++ show op ++ ")") + +------------------------------------------------------------------------------ +-- Driver + +mainEval :: Code -> IO () +mainEval prog = do { stk <- eval (State emptyEnv [] prog) + ; return () + } +{- + * Oops, one of the example actually has something + * on the stack at the end. + * Oh well... + ; if null stk + then return () + else do { putStrLn done + ; print stk + } +-} + +done = "Items still on stack at (successfull) termination of program" + +------------------------------------------------------------------------------ +-- testing + +test :: String -> Pure Stack +test is = eval (State emptyEnv [] (rayParse is)) + +testF :: String -> IO Stack +testF is = do prog <- rayParseF is + eval (State emptyEnv [] prog) + +testA :: String -> Either String (Stack,Int) +testA is = case runAbs (eval (State emptyEnv + [VAbsObj AbsFACE,VAbsObj AbsU,VAbsObj AbsV] + (rayParse is))) 100 of + AbsState a n -> Right (a,n) + AbsFail m -> Left m + +abstest1 = "1.0 0.0 0.0 point /red { /v /u /face red 1.0 0.0 1.0 } apply" + +-- should be [3:: Int] +et1 = test "1 /x { x } /f 2 /x f apply x addi" + + + + + diff --git a/ghc/tests/programs/galois_raytrace/Geometry.hs b/ghc/tests/programs/galois_raytrace/Geometry.hs new file mode 100644 index 000000000000..673c7d4812af --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Geometry.hs @@ -0,0 +1,314 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Geometry + ( Coords + , Ray + , Point -- abstract + , Vector -- abstract + , Matrix -- abstract + , Color -- abstract + , Box(..) + , Radian + , matrix + , coord + , color + , uncolor + , xCoord , yCoord , zCoord + , xComponent , yComponent , zComponent + , point + , vector + , nearV + , point_to_vector + , vector_to_point + , dot + , cross + , tangents + , addVV + , addPV + , subVV + , negV + , subPP + , norm + , normalize + , dist2 + , sq + , distFrom0Sq + , distFrom0 + , multSV + , multMM + , transposeM + , multMV + , multMP + , multMQ + , multMR + , white + , black + , addCC + , subCC + , sumCC + , multCC + , multSC + , nearC + , offsetToPoint + , epsilon + , inf + , nonZero + , eqEps + , near + , clampf + ) where + +import List + +type Coords = (Double,Double,Double) + +type Ray = (Point,Vector) -- origin of ray, and unit vector giving direction + +data Point = P !Double !Double !Double -- implicit extra arg of 1 + deriving (Show) +data Vector = V !Double !Double !Double -- implicit extra arg of 0 + deriving (Show, Eq) +data Matrix = M !Quad !Quad !Quad !Quad + deriving (Show) + +data Color = C !Double !Double !Double + deriving (Show, Eq) + +data Box = B !Double !Double !Double !Double !Double !Double + deriving (Show) + +data Quad = Q !Double !Double !Double !Double + deriving (Show) + +type Radian = Double + +type Tup4 a = (a,a,a,a) + +--{-# INLINE matrix #-} +matrix :: Tup4 (Tup4 Double) -> Matrix +matrix ((m11, m12, m13, m14), + (m21, m22, m23, m24), + (m31, m32, m33, m34), + (m41, m42, m43, m44)) + = M (Q m11 m12 m13 m14) + (Q m21 m22 m23 m24) + (Q m31 m32 m33 m34) + (Q m41 m42 m43 m44) + +coord x y z = (x, y, z) + +color r g b = C r g b + +uncolor (C r g b) = (r,g,b) + +{-# INLINE xCoord #-} +xCoord (P x y z) = x +{-# INLINE yCoord #-} +yCoord (P x y z) = y +{-# INLINE zCoord #-} +zCoord (P x y z) = z + +{-# INLINE xComponent #-} +xComponent (V x y z) = x +{-# INLINE yComponent #-} +yComponent (V x y z) = y +{-# INLINE zComponent #-} +zComponent (V x y z) = z + +point :: Double -> Double -> Double -> Point +point x y z = P x y z + +vector :: Double -> Double -> Double -> Vector +vector x y z = V x y z + +nearV :: Vector -> Vector -> Bool +nearV (V a b c) (V d e f) = a `near` d && b `near` e && c `near` f + +point_to_vector :: Point -> Vector +point_to_vector (P x y z) = V x y z + +vector_to_point :: Vector -> Point +vector_to_point (V x y z) = P x y z + +{-# INLINE vector_to_quad #-} +vector_to_quad :: Vector -> Quad +vector_to_quad (V x y z) = Q x y z 0 + +{-# INLINE point_to_quad #-} +point_to_quad :: Point -> Quad +point_to_quad (P x y z) = Q x y z 1 + +{-# INLINE quad_to_point #-} +quad_to_point :: Quad -> Point +quad_to_point (Q x y z _) = P x y z + +{-# INLINE quad_to_vector #-} +quad_to_vector :: Quad -> Vector +quad_to_vector (Q x y z _) = V x y z + +--{-# INLINE dot #-} +dot :: Vector -> Vector -> Double +dot (V x1 y1 z1) (V x2 y2 z2) = x1 * x2 + y1 * y2 + z1 * z2 + +cross :: Vector -> Vector -> Vector +cross (V x1 y1 z1) (V x2 y2 z2) + = V (y1 * z2 - z1 * y2) (z1 * x2 - x1 * z2) (x1 * y2 - y1 * x2) + +-- assumption: the input vector is a normal +tangents :: Vector -> (Vector, Vector) +tangents v@(V x y z) + = (v1, v `cross` v1) + where v1 | x == 0 = normalize (vector 0 z (-y)) + | otherwise = normalize (vector (-y) x 0) + +{-# INLINE dot4 #-} +dot4 :: Quad -> Quad -> Double +dot4 (Q x1 y1 z1 w1) (Q x2 y2 z2 w2) = x1 * x2 + y1 * y2 + z1 * z2 + w1 * w2 + +addVV :: Vector -> Vector -> Vector +addVV (V x1 y1 z1) (V x2 y2 z2) + = V (x1 + x2) (y1 + y2) (z1 + z2) + +addPV :: Point -> Vector -> Point +addPV (P x1 y1 z1) (V x2 y2 z2) + = P (x1 + x2) (y1 + y2) (z1 + z2) + +subVV :: Vector -> Vector -> Vector +subVV (V x1 y1 z1) (V x2 y2 z2) + = V (x1 - x2) (y1 - y2) (z1 - z2) + +negV :: Vector -> Vector +negV (V x1 y1 z1) + = V (-x1) (-y1) (-z1) + +subPP :: Point -> Point -> Vector +subPP (P x1 y1 z1) (P x2 y2 z2) + = V (x1 - x2) (y1 - y2) (z1 - z2) + +--{-# INLINE norm #-} +norm :: Vector -> Double +norm (V x y z) = sqrt (sq x + sq y + sq z) + +--{-# INLINE normalize #-} +-- normalize a vector to a unit vector +normalize :: Vector -> Vector +normalize v@(V x y z) + | norm /= 0 = multSV (1/norm) v + | otherwise = error "normalize empty!" + where norm = sqrt (sq x + sq y + sq z) + +-- This does computes the distance *squared* +dist2 :: Point -> Point -> Double +dist2 us vs = sq x + sq y + sq z + where + (V x y z) = subPP us vs + +{-# INLINE sq #-} +sq :: Double -> Double +sq d = d * d + +{-# INLINE distFrom0Sq #-} +distFrom0Sq :: Point -> Double -- Distance of point from origin. +distFrom0Sq (P x y z) = sq x + sq y + sq z + +{-# INLINE distFrom0 #-} +distFrom0 :: Point -> Double -- Distance of point from origin. +distFrom0 p = sqrt (distFrom0Sq p) + +--{-# INLINE multSV #-} +multSV :: Double -> Vector -> Vector +multSV k (V x y z) = V (k*x) (k*y) (k*z) + +--{-# INLINE multMM #-} +multMM :: Matrix -> Matrix -> Matrix +multMM m1@(M q1 q2 q3 q4) m2 + = M (multMQ m2' q1) + (multMQ m2' q2) + (multMQ m2' q3) + (multMQ m2' q4) + where + m2' = transposeM m2 + +{-# INLINE transposeM #-} +transposeM :: Matrix -> Matrix +transposeM (M (Q e11 e12 e13 e14) + (Q e21 e22 e23 e24) + (Q e31 e32 e33 e34) + (Q e41 e42 e43 e44)) = (M (Q e11 e21 e31 e41) + (Q e12 e22 e32 e42) + (Q e13 e23 e33 e43) + (Q e14 e24 e34 e44)) + + +--multMM m1 m2 = [map (dot4 row) (transpose m2) | row <- m1] + +--{-# INLINE multMV #-} +multMV :: Matrix -> Vector -> Vector +multMV m v = quad_to_vector (multMQ m (vector_to_quad v)) + +--{-# INLINE multMP #-} +multMP :: Matrix -> Point -> Point +multMP m p = quad_to_point (multMQ m (point_to_quad p)) + +-- mat vec = map (dot4 vec) mat + +{-# INLINE multMQ #-} +multMQ :: Matrix -> Quad -> Quad +multMQ (M q1 q2 q3 q4) q + = Q (dot4 q q1) + (dot4 q q2) + (dot4 q q3) + (dot4 q q4) + +{-# INLINE multMR #-} +multMR :: Matrix -> Ray -> Ray +multMR m (r, v) = (multMP m r, multMV m v) + +white :: Color +white = C 1 1 1 +black :: Color +black = C 0 0 0 + +addCC :: Color -> Color -> Color +addCC (C a b c) (C d e f) = C (a+d) (b+e) (c+f) + +subCC :: Color -> Color -> Color +subCC (C a b c) (C d e f) = C (a-d) (b-e) (c-f) + +sumCC :: [Color] -> Color +sumCC = foldr addCC black + +multCC :: Color -> Color -> Color +multCC (C a b c) (C d e f) = C (a*d) (b*e) (c*f) + +multSC :: Double -> Color -> Color +multSC k (C a b c) = C (a*k) (b*k) (c*k) + +nearC :: Color -> Color -> Bool +nearC (C a b c) (C d e f) = a `near` d && b `near` e && c `near` f + +offsetToPoint :: Ray -> Double -> Point +offsetToPoint (r,v) i = r `addPV` (i `multSV` v) + +-- + +epsilon, inf :: Double -- aproximate zero and infinity +epsilon = 1.0e-10 +inf = 1.0e20 + +nonZero :: Double -> Double -- Use before a division. It makes definitions +nonZero x | x > epsilon = x -- more complete and I bet the errors that get + | x < -epsilon = x -- introduced will be undetectable if epsilon + | otherwise = epsilon -- is small enough + + +eqEps x y = abs (x-y) < epsilon +near = eqEps + +clampf :: Double -> Double +clampf p | p < 0 = 0 + | p > 1 = 1 + | True = p diff --git a/ghc/tests/programs/galois_raytrace/Illumination.hs b/ghc/tests/programs/galois_raytrace/Illumination.hs new file mode 100644 index 000000000000..9242cbf7a416 --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Illumination.hs @@ -0,0 +1,212 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +-- Modified to use stdout (for testing) + +module Illumination + ( Object + , Light (..) + , light, pointlight, spotlight + , render + ) where + +import Array +import Char(chr) +import IOExts +import Maybe + +import Geometry +import CSG +import Surface +import Misc + +type Object = CSG (SurfaceFn Color Double) + +data Cxt = Cxt {ambient::Color, lights::[Light], object::Object, depth::Int} + deriving Show + +render :: (Matrix,Matrix) -> Color -> [Light] -> Object -> Int -> + Radian -> Int -> Int -> String -> IO () +render (m,m') amb ls obj dep fov wid ht file + = do { debugging + ; putStrLn (showBitmap wid ht pixels) + } + where + debugging = return () +{- + do { putStrLn (show cxt) + ; putStrLn (show (width, delta, aspect, left, top)) + } +-} + obj' = transform (m',m) obj + ls' = [ transformLight m' l | l <- ls ] + pixelA = listArray ((1,1), (ht,wid)) + [ illumination cxt (start,pixel i j) + | j <- take ht [0.5..] + , i <- take wid [0.5..] ] + antiA = pixelA // + [ (ix, superSample ix (pixelA ! ix)) + | j <- [2 .. ht - 1], i <- [2 .. wid - 1] + , let ix = (j, i) + , contrast ix pixelA ] + pixels = [ [ illumination cxt (start,pixel i j) | i<- take wid [0.5..] ] + | j <- take ht [0.5..] + ] + cxt = Cxt {ambient=amb, lights=ls', object=obj', depth=dep} + start = point 0 0 (-1) + width = 2 * tan (fov/2) + delta = width / fromIntegral wid + aspect = fromIntegral ht / fromIntegral wid + left = - width / 2 + top = - left * aspect + pixel i j = vector (left + i*delta) (top - j*delta) 1 + + superSample (y, x) col = avg $ col: + [ illumination cxt (start, pixel (fromIntegral x - 0.5 + xd) (fromIntegral y - 0.5 + yd)) + | (xd, yd) <- [(-0.333, 0.0), (0.333, 0.0), (0.0, -0.333), (0.0, 0.333)] + ] + +avg cs = divN (fromIntegral (length cs)) (uncolor (sumCC cs)) + where divN n (r,g,b) = color (r / n) (g / n) (b / n) + +contrast :: (Int, Int) -> Array (Int, Int) Color -> Bool +contrast (x, y) arr = any diffMax [ subCC cur (arr ! (x + xd, y + yd)) + | xd <- [-1, 1], yd <- [-1, 1] + ] + where cur = arr ! (x, y) + diffMax col = (abs r) > 0.25 || (abs g) > 0.2 || (abs b) > 0.4 + where + (r,g,b) = uncolor col + + +illumination :: Cxt -> Ray -> Color +illumination cxt (r,v) + | depth cxt <= 0 = black + | otherwise = case castRay (r,v) (object cxt) of + Nothing -> black + Just info -> illum (cxt{depth=(depth cxt)-1}) info v + +illum :: Cxt -> (Point,Vector,Properties Color Double) -> Vector -> Color +illum cxt (pos,normV,(col,kd,ks,n)) v + = ambTerm `addCC` difTerm `addCC` spcTerm `addCC` recTerm + where + visibleLights = unobscured pos (object cxt) (lights cxt) normV + d = depth cxt + amb = ambient cxt + newV = subVV v (multSV (2 * dot normV v) normV) + + ambTerm = multSC kd (multCC amb col) + difTerm = multSC kd (sumCC [multSC (dot normV lj) (multCC intensity col) + |(loc,intensity) <- visibleLights, + let lj = normalize ({- pos `subVV` -} loc)]) + -- ZZ might want to avoid the phong, when you can... + spcTerm = multSC ks (sumCC [multSC ((dot normV hj) ** n ) (multCC intensity col) + |(loc,intensity) <- visibleLights, + -- ZZ note this is specific to the light at infinity + let lj = {- pos `subVV` -} normalize loc, + let hj = normalize (lj `subVV` normalize v)]) + recTerm = if recCoeff `nearC` black then black else multCC recCoeff recRay + recCoeff = multSC ks col + recRay = illumination cxt (pos,newV) + +showBitmapA :: Int -> Int -> Array (Int, Int) Color -> String +showBitmapA wid ht arr + = header ++ concatMap scaleColor (elems arr) + where + scaleColor col = [scalePixel r, scalePixel g, scalePixel b] + where (r,g,b) = uncolor col + header = "P6\n#Galois\n" ++ show wid ++ " " ++ show ht ++ "\n255\n" + +showBitmap :: Int -> Int ->[[Color]] -> String +showBitmap wid ht pss +-- type of assert | length pss == ht && all (\ ps -> length ps == wid) pss + = header ++ concat [[scalePixel r,scalePixel g,scalePixel b] + | ps <- pss, (r,g,b) <- map uncolor ps] + where + header = "P6\n#Galois\n" ++ show wid ++ " " ++ show ht ++ "\n255\n" +showBitmap _ _ _ = error "incorrect length of bitmap string" + +scalePixel :: Double -> Char +scalePixel p = chr (floor (clampf p * 255)) + + +-- Lights + +data Light = Light Vector Color + | PointLight Point Color + | SpotLight Point Point Color Radian Double + deriving Show + +light :: Coords -> Color -> Light +light (x,y,z) color = + Light (normalize (vector (-x) (-y) (-z))) color +pointlight (x,y,z) color = + PointLight (point x y z) color +spotlight (x,y,z) (p,q,r) col cutoff exp = + SpotLight (point x y z) (point p q r) col cutoff exp + +transformLight m (Light v c) = Light (multMV m v) c +transformLight m (PointLight p c) = PointLight (multMP m p) c +transformLight m (SpotLight p q c r d) = SpotLight (multMP m p) (multMP m q) c r d + +unobscured :: Point -> Object -> [Light] -> Vector -> [(Vector,Color)] +unobscured pos obj lights normV = catMaybes (map (unobscure pos obj normV) lights) + +unobscure :: Point -> Object -> Vector -> Light -> Maybe (Vector,Color) +unobscure pos obj normV (Light vec color) + -- ZZ probably want to make this faster + | vec `dot` normV < 0 = Nothing + | intersects (pos `addPV` (0.0001 `multSV` vec),vec) obj = Nothing + | otherwise = Just (vec,color) +unobscure pos obj normV (PointLight pp color) + | vec `dot` normV < 0 = Nothing + | intersectWithin (pos `addPV` (0.0001 `multSV` (normalize vec)), vec) obj = Nothing + | otherwise = Just (vec,is) + where vec = pp `subPP` pos + is = attenuate vec color +unobscure org obj normV (SpotLight pos at color cutoff exp) + | vec `dot` normV < 0 = Nothing + | intersectWithin (org `addPV` (0.0001 `multSV` (normalize vec)), vec) obj = Nothing + | angle > cutoff = Nothing + | otherwise = Just (vec, is) + where vec = pos `subPP` org + vec' = pos `subPP` at + angle = acos (normalize vec `dot` (normalize vec')) + + asp = normalize (at `subPP` pos) + qsp = normalize (org `subPP` pos) + is = attenuate vec (((asp `dot` qsp) ** exp) `multSC` color) + +attenuate :: Vector -> Color -> Color +attenuate vec color = (100 / (99 + sq (norm vec))) `multSC` color + +-- + +castRay ray p + = case intersectRayWithObject ray p of + (True, _, _) -> Nothing -- eye is inside + (False, [], _) -> Nothing -- eye is inside + (False, (0, b, _) : _, _) -> Nothing -- eye is inside + (False, (i, False, _) : _, _) -> Nothing -- eye is inside + (False, (t, b, (s, p0)) : _, _) -> + let (v, prop) = surface s p0 in + Just (offsetToPoint ray t, v, prop) + +intersects ray p + = case intersectRayWithObject ray p of + (True, _, _) -> False + (False, [], _) -> False + (False, (0, b, _) : _, _) -> False + (False, (i, False, _) : _, _) -> False + (False, (i, b, _) : _, _) -> True + +intersectWithin :: Ray -> Object -> Bool +intersectWithin ray p + = case intersectRayWithObject ray p of + (True, _, _) -> False -- eye is inside + (False, [], _) -> False -- eye is inside + (False, (0, b, _) : _, _) -> False -- eye is inside + (False, (i, False, _) : _, _) -> False -- eye is inside + (False, (t, b, _) : _, _) -> t < 1.0 diff --git a/ghc/tests/programs/galois_raytrace/Intersections.hs b/ghc/tests/programs/galois_raytrace/Intersections.hs new file mode 100644 index 000000000000..c7fe003eb338 --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Intersections.hs @@ -0,0 +1,404 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Intersections + ( intersectRayWithObject, + quadratic + ) where + +import Maybe(isJust) + +import Construct +import Geometry +import Interval +import Misc + +-- This is factored into two bits. The main function `intersections' +-- intersects a line with an object. +-- The wrapper call `intersectRayWithObject' coerces this to an intersection +-- with a ray by clamping the result to start at 0. + +intersectRayWithObject ray p + = clampIntervals is + where is = intersections ray p + +clampIntervals (True, [], True) = (False, [(0, True, undefined)], True) +clampIntervals empty@(False, [], False) = empty +clampIntervals (True, is@((i, False, p) : is'), isOpen) + | i `near` 0 || i < 0 + = clampIntervals (False, is', isOpen) + | otherwise + = (False, (0, True, undefined) : is, isOpen) +clampIntervals ivals@(False, is@((i, True, p) : is'), isOpen) + | i `near` 0 || i < 0 + -- can unify this with first case... + = clampIntervals (True, is', isOpen) + | otherwise + = ivals + +intersections ray (Union p q) + = unionIntervals is js + where is = intersections ray p + js = intersections ray q + +intersections ray (Intersect p q) + = intersectIntervals is js + where is = intersections ray p + js = intersections ray q + +intersections ray (Difference p q) + = differenceIntervals is (negateSurfaces js) + where is = intersections ray p + js = intersections ray q + +intersections ray (Transform m m' p) + = mapI (xform m) is + where is = intersections (m' `multMR` ray) p + xform m (i, b, (s, p0)) = (i, b, (transformSurface m s, p0)) + +intersections ray (Box box p) + | intersectWithBox ray box = intersections ray p + | otherwise = emptyIList + +intersections ray p@(Plane s) + = intersectPlane ray s + +intersections ray p@(Sphere s) + = intersectSphere ray s + +intersections ray p@(Cube s) + = intersectCube ray s + +intersections ray p@(Cylinder s) + = intersectCylinder ray s + +intersections ray p@(Cone s) + = intersectCone ray s + +negateSurfaces :: IList (Surface, Texture a) -> IList (Surface, Texture a) +negateSurfaces = mapI negSurf + where negSurf (i, b, (s,t)) = (i, b, (negateSurface s, t)) + +negateSurface (Planar p0 v0 v1) + = Planar p0 v1 v0 +negateSurface (Spherical p0 v0 v1) + = Spherical p0 v1 v0 +negateSurface (Cylindrical p0 v0 v1) + = Cylindrical p0 v1 v0 +negateSurface (Conic p0 v0 v1) + = Conic p0 v1 v0 + +transformSurface m (Planar p0 v0 v1) + = Planar p0' v0' v1' + where p0' = multMP m p0 + v0' = multMV m v0 + v1' = multMV m v1 + +transformSurface m (Spherical p0 v0 v1) + = Spherical p0' v0' v1' + where p0' = multMP m p0 + v0' = multMV m v0 + v1' = multMV m v1 + +-- ditto as above +transformSurface m (Cylindrical p0 v0 v1) + = Cylindrical p0' v0' v1' + where p0' = multMP m p0 + v0' = multMV m v0 + v1' = multMV m v1 + +transformSurface m (Conic p0 v0 v1) + = Conic p0' v0' v1' + where p0' = multMP m p0 + v0' = multMV m v0 + v1' = multMV m v1 + +-------------------------------- +-- Plane +-------------------------------- + +intersectPlane :: Ray -> a -> IList (Surface, Texture a) +intersectPlane ray texture = intersectXZPlane PlaneFace ray 0.0 texture + +intersectXZPlane :: Face -> Ray -> Double -> a -> IList (Surface, Texture a) +intersectXZPlane n (r,v) yoffset texture + | b `near` 0 + = -- the ray is parallel to the plane - it's either all in, or all out + if y `near` yoffset || y < yoffset then openIList else emptyIList + + -- The line intersects the plane. Find t such that + -- (x + at, y + bt, z + ct) intersects the X-Z plane. + -- t may be negative (the ray starts within the halfspace), + -- but we'll catch that later when we clamp the intervals + + | b < 0 -- the ray is pointing downwards + = (False, [mkEntry (t0, (Planar p0 v0 v1, (n, p0, texture)))], True) + + | otherwise -- the ray is pointing upwards + = (True, [mkExit (t0, (Planar p0 v0 v1, (n, p0, texture)))], False) + + where t0 = (yoffset-y) / b + x0 = x + a * t0 + z0 = z + c * t0 + p0 = point x0 0 z0 + v0 = vector 0 0 1 + v1 = vector 1 0 0 + + x = xCoord r + y = yCoord r + z = zCoord r + a = xComponent v + b = yComponent v + c = zComponent v + + +-------------------------------- +-- Sphere +-------------------------------- + +intersectSphere :: Ray -> a -> IList (Surface, Texture a) +intersectSphere ray@(r, v) texture + = -- Find t such that (x + ta, y + tb, z + tc) intersects the + -- unit sphere, that is, such that: + -- (x + ta)^2 + (y + tb)^2 + (z + tc)^2 = 1 + -- This is a quadratic equation in t: + -- t^2(a^2 + b^2 + c^2) + 2t(xa + yb + zc) + (x^2 + y^2 + z^2 - 1) = 0 + let c1 = sq a + sq b + sq c + c2 = 2 * (x * a + y * b + z * c) + c3 = sq x + sq y + sq z - 1 + in + case quadratic c1 c2 c3 of + Nothing -> emptyIList + Just (t1, t2) -> entryexit (g t1) (g t2) + where x = xCoord r + y = yCoord r + z = zCoord r + a = xComponent v + b = yComponent v + c = zComponent v + g t = (t, (Spherical origin v1 v2, (SphereFace, p0, texture))) + where origin = point 0 0 0 + x0 = x + t * a + y0 = y + t * b + z0 = z + t * c + p0 = point x0 y0 z0 + v0 = vector x0 y0 z0 + (v1, v2) = tangents v0 + + +-------------------------------- +-- Cube +-------------------------------- + +intersectCube :: Ray -> a -> IList (Surface, Texture a) +intersectCube ray@(r, v) texture + = -- The set of t such that (x + at, y + bt, z + ct) lies within + -- the unit cube satisfies: + -- 0 <= x + at <= 1, 0 <= y + bt <= 1, 0 <= z + ct <= 1 + -- The minimum and maximum such values of t give us the two + -- intersection points. + case intersectSlabIval (intersectCubeSlab face2 face3 x a) + (intersectSlabIval (intersectCubeSlab face5 face4 y b) + (intersectCubeSlab face0 face1 z c)) of + Nothing -> emptyIList + Just (t1, t2) -> entryexit (g t1) (g t2) + where g ((n, v0, v1), t) + = (t, (Planar p0 v0 v1, (n, p0, texture))) + where p0 = offsetToPoint ray t + face0 = (CubeFront, vectorY, vectorX) + face1 = (CubeBack, vectorX, vectorY) + face2 = (CubeLeft, vectorZ, vectorY) + face3 = (CubeRight, vectorY, vectorZ) + face4 = (CubeTop, vectorZ, vectorX) + face5 = (CubeBottom, vectorX, vectorZ) + vectorX = vector 1 0 0 + vectorY = vector 0 1 0 + vectorZ = vector 0 0 1 + x = xCoord r + y = yCoord r + z = zCoord r + a = xComponent v + b = yComponent v + c = zComponent v + +intersectCubeSlab n m w d + | d `near` 0 = if (0 <= w) && (w <= 1) + then Just ((n, -inf), (m, inf)) else Nothing + | d > 0 = Just ((n, (-w)/d), (m, (1-w)/d)) + | otherwise = Just ((m, (1-w)/d), (n, (-w)/d)) + +intersectSlabIval Nothing Nothing = Nothing +intersectSlabIval Nothing (Just i) = Nothing +intersectSlabIval (Just i) Nothing = Nothing +intersectSlabIval (Just (nu1@(n1, u1), mv1@(m1, v1))) + (Just (nu2@(n2, u2), mv2@(m2, v2))) + = checkInterval (nu, mv) + where nu = if u1 < u2 then nu2 else nu1 + mv = if v1 < v2 then mv1 else mv2 + checkInterval numv@(nu@(_, u), (m, v)) + -- rounding error may force us to push v out a bit + | u `near` v = Just (nu, (m, u + epsilon)) + | u < v = Just numv + | otherwise = Nothing + + +-------------------------------- +-- Cylinder +-------------------------------- + +intersectCylinder :: Ray -> a -> IList (Surface, Texture a) +intersectCylinder ray texture + = isectSide `intersectIntervals` isectTop `intersectIntervals` isectBottom + where isectSide = intersectCylSide ray texture + isectTop = intersectXZPlane CylinderTop ray 1.0 texture + isectBottom = complementIntervals $ negateSurfaces $ + intersectXZPlane CylinderBottom ray 0.0 texture + +intersectCylSide (r, v) texture + = -- The ray (x + ta, y + tb, z + tc) intersects the sides of the + -- cylinder if: + -- (x + ta)^2 + (z + tc)^2 = 1 and 0 <= y + tb <= 1. + if (sq a + sq c) `near` 0 + then -- The ray is parallel to the Y-axis, and does not intersect + -- the cylinder sides. It's either all in, or all out + if (sqxy `near` 1.0 || sqxy < 1.0) then openIList else emptyIList + else -- Find values of t that solve the quadratic equation + -- (a^2 + c^2)t^2 + 2(ax + cz)t + x^2 + z^2 - 1 = 0 + let c1 = sq a + sq c + c2 = 2 * (x * a + z * c) + c3 = sq x + sq z - 1 + in + case quadratic c1 c2 c3 of + Nothing -> emptyIList + Just (t1, t2) -> entryexit (g t1) (g t2) + + where sqxy = sq x + sq y + g t = (t, (Cylindrical origin v1 v2, (CylinderSide, p0, texture))) + where origin = point 0 0 0 + x0 = x + t * a + y0 = y + t * b + z0 = z + t * c + p0 = point x0 y0 z0 + v0 = vector x0 0 z0 + (v1, v2) = tangents v0 + + x = xCoord r + y = yCoord r + z = zCoord r + a = xComponent v + b = yComponent v + c = zComponent v + + +------------------- +-- Cone +------------------- + +intersectCone :: Ray -> a -> IList (Surface, Texture a) +intersectCone ray texture + = isectSide `intersectIntervals` isectTop `intersectIntervals` isectBottom + where isectSide = intersectConeSide ray texture + isectTop = intersectXZPlane ConeBase ray 1.0 texture + isectBottom = complementIntervals $ negateSurfaces $ + intersectXZPlane ConeBase ray 0.0 texture + +intersectConeSide (r, v) texture + = -- Find the points where the ray intersects the cond side. At any points of + -- intersection, we must have: + -- (x + ta)^2 + (z + tc)^2 = (y + tb)^2 + -- which is the following quadratic equation: + -- t^2(a^2-b^2+c^2) + 2t(xa-yb+cz) + (x^2-y^2+z^2) = 0 + let c1 = sq a - sq b + sq c + c2 = 2 * (x * a - y * b + c * z) + c3 = sq x - sq y + sq z + in case quadratic c1 c2 c3 of + Nothing -> emptyIList + Just (t1, t2) -> + -- If either intersection strikes the middle, then the other + -- can only be off by rounding error, so we make a tangent + -- strike using the "good" value. + -- If the intersections straddle the origin, then it's + -- an exit/entry pair, otherwise it's an entry/exit pair. + let y1 = y + t1 * b + y2 = y + t2 * b + in if y1 `near` 0 then entryexit (g t1) (g t1) + else if y2 `near` 0 then entryexit (g t2) (g t2) + else if (y1 < 0) `xor` (y2 < 0) then exitentry (g t1) (g t2) + else entryexit (g t1) (g t2) + + where g t = (t, (Conic origin v1 v2, (ConeSide, p0, texture))) + where origin = point 0 0 0 + x0 = x + t * a + y0 = y + t * b + z0 = z + t * c + p0 = point x0 y0 z0 + v0 = normalize $ vector x0 (-y0) z0 + (v1, v2) = tangents v0 + + x = xCoord r + y = yCoord r + z = zCoord r + a = xComponent v + b = yComponent v + c = zComponent v + + -- beyond me why this isn't defined in the prelude... + xor False b = b + xor True b = not b + + +------------------- +-- Solving quadratics +------------------- + +quadratic :: Double -> Double -> Double -> Maybe (Double, Double) +quadratic a b c = + -- Solve the equation ax^2 + bx + c = 0 by using the quadratic formula. + let d = sq b - 4 * a * c + d' = if d `near` 0 then 0 else d + in if d' < 0 + then Nothing -- There are no real roots. + else + if a > 0 then Just (((-b) - sqrt d') / (2 * a), + ((-b) + sqrt d') / (2 * a)) + else Just (((-b) + sqrt d') / (2 * a), + ((-b) - sqrt d') / (2 * a)) + +------------------- +-- Bounding boxes +------------------- + +data MaybeInterval = Interval !Double !Double + | NoInterval + +isInterval (Interval _ _) = True +isInterval _ = False + +intersectWithBox :: Ray -> Box -> Bool +intersectWithBox (r, v) (B x1 x2 y1 y2 z1 z2) + = isInterval interval + where x_interval = intersectRayWithSlab (xCoord r) (xComponent v) (x1, x2) + y_interval = intersectRayWithSlab (yCoord r) (yComponent v) (y1, y2) + z_interval = intersectRayWithSlab (zCoord r) (zComponent v) (z1, z2) + interval = intersectInterval x_interval + (intersectInterval y_interval z_interval) + +intersectInterval :: MaybeInterval -> MaybeInterval -> MaybeInterval +intersectInterval NoInterval _ = NoInterval +intersectInterval _ NoInterval = NoInterval +intersectInterval (Interval a b) (Interval c d) + | b < c || d < a = NoInterval + | otherwise = Interval (a `max` c) (b `min` d) + +{-# INLINE intersectRayWithSlab #-} +intersectRayWithSlab :: Double -> Double -> (Double,Double) -> MaybeInterval +intersectRayWithSlab xCoord alpha (x1, x2) + | alpha == 0 = if xCoord < x1 || xCoord > x2 then NoInterval else infInterval + | alpha > 0 = Interval a b + | otherwise = Interval b a + where a = (x1 - xCoord) / alpha + b = (x2 - xCoord) / alpha + +infInterval = Interval (-inf) inf diff --git a/ghc/tests/programs/galois_raytrace/Interval.hs b/ghc/tests/programs/galois_raytrace/Interval.hs new file mode 100644 index 000000000000..a4d313f66e7b --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Interval.hs @@ -0,0 +1,121 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Interval + ( IList + , Intersection + , emptyIList, openIList + , mkEntry, mkExit + , entryexit, exitentry + , mapI + , unionIntervals, intersectIntervals, differenceIntervals + , complementIntervals + ) where + +import Geometry + +-- The result of a ray trace is represented as a list of surface +-- intersections. Each intersection is a point along the ray with +-- a flag indicating whether this intersection is an entry or an +-- exit from the solid. Each intersection also carries unspecified +-- surface data for use by the illumination model. + +-- Just the list of intersections isn't enough, however. An empty +-- list can denote either a trace that is always within the solid +-- or never in the solid. To dissambiguate, an extra flag is kept +-- that indicates whether we are starting inside or outside of the +-- solid. As a convenience, we also keep an additional flag that +-- indicates whether the last intersection ends inside or outside. + +type IList a = (Bool, [Intersection a], Bool) +type Intersection a = (Double, Bool, a) + +emptyIList = (False, [], False) +openIList = (True, [], True) + +mapI f (b1, is, b2) = (b1, map f is, b2) + +isEntry (_, entry, _) = entry +isExit (_, entry, _) = not entry + +mkEntry (t, a) = (t, True, a) +mkExit (t, a) = (t, False, a) + +entryexit w1 w2 = (False, [mkEntry w1, mkExit w2], False) +exitentry w1 w2 = (True, [mkExit w1, mkEntry w2], True) +arrange w1@(t1, _) w2@(t2, _) | t1 < t2 = entryexit w1 w2 + | otherwise = entryexit w2 w1 + + +cmpI :: Intersection a -> Intersection a -> Ordering +cmpI (i, _, _) (j, _, _) + | i `near` j = EQ + | i < j = LT + | otherwise = GT + +bad (b1, [], b2) = b1 /= b2 +bad (b1, is, b2) = bad' b1 is || b2 /= b3 + where (_, b3, _) = last is + +bad' b [] = False +bad' b ((_, c, _) : is) = b == c || bad' c is + +unionIntervals :: IList a -> IList a -> IList a +unionIntervals (isStartOpen, is, isEndOpen) (jsStartOpen, js, jsEndOpen) + = (isStartOpen || jsStartOpen, uniIntervals is js, isEndOpen || jsEndOpen) + where uniIntervals is [] | jsEndOpen = [] + | otherwise = is + uniIntervals [] js | isEndOpen = [] + | otherwise = js + uniIntervals is@(i : is') js@(j : js') + = case cmpI i j of + EQ -> if isEntry i == isEntry j then i : uniIntervals is' js' + else uniIntervals is' js' + LT -> if isEntry j then i : uniIntervals is' js + else uniIntervals is' js + GT -> if isEntry i then j : uniIntervals is js' + else uniIntervals is js' + +intersectIntervals :: IList a -> IList a -> IList a +intersectIntervals is js + = complementIntervals (unionIntervals is' js') + where is' = complementIntervals is + js' = complementIntervals js + +differenceIntervals :: IList a -> IList a -> IList a +differenceIntervals is js + = complementIntervals (unionIntervals is' js) + where is' = complementIntervals is + +complementIntervals :: IList a -> IList a +complementIntervals (o1, is, o2) + = (not o1, [ (i, not isentry, a) | (i, isentry, a) <- is ], not o2) + +-- tests... + +{- +mkIn, mkOut :: Double -> Intersection a +mkIn x = (x, True, undefined) +mkOut x = (x, False, undefined) + +i1 = (False, [ mkIn 2, mkOut 7 ], False) +i1' = (True, [ mkOut 2, mkIn 7 ], True) +i2 = (False, [ mkIn 1, mkOut 3, mkIn 4, mkOut 5, mkIn 6, mkOut 8 ], False) + +t1 = unionIntervals i1 i2 +t2 = intersectIntervals i1 i2 +t3 = intersectIntervals i2 i1 +t4 = complementIntervals i1 +t5 = intersectIntervals i2 i1' +t6 = differenceIntervals i2 i1 +t7 = differenceIntervals i2 i2 + +sh (o1,is,o2) = + do if o1 then putStr "..." else return () + putStr $ foldr1 (++) (map si is) + if o2 then putStr "..." else return () +si (i, True, _, _) = "<" ++ show i +si (i, False, _, _) = " " ++ show i ++ ">" +-} diff --git a/ghc/tests/programs/galois_raytrace/Main.hs b/ghc/tests/programs/galois_raytrace/Main.hs new file mode 100644 index 000000000000..4ef9fe3e95a9 --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Main.hs @@ -0,0 +1,17 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +-- Modified to read sample input directly from a file. + +module Main where + +import System + +import Parse +import Eval + +main = do { str <- readFile "galois.gml" + ; mainEval (rayParse str) + } diff --git a/ghc/tests/programs/galois_raytrace/Makefile b/ghc/tests/programs/galois_raytrace/Makefile new file mode 100644 index 000000000000..f181efd9171f --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Makefile @@ -0,0 +1,9 @@ +TOP = .. +include $(TOP)/mk/boilerplate.mk + +SRC_HC_OPTS += -package text -package lang + +all :: runtest + +include $(TOP)/mk/target.mk + diff --git a/ghc/tests/programs/galois_raytrace/Misc.hs b/ghc/tests/programs/galois_raytrace/Misc.hs new file mode 100644 index 000000000000..1368b31f3f6c --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Misc.hs @@ -0,0 +1,11 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Misc where + +import IOExts + +debug s v = trace (s ++" : "++ show v ++ "\n") v +-- debug s v = v diff --git a/ghc/tests/programs/galois_raytrace/Parse.hs b/ghc/tests/programs/galois_raytrace/Parse.hs new file mode 100644 index 000000000000..10b9f9b2f8e6 --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Parse.hs @@ -0,0 +1,137 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Parse where + +import Char +import Parsec hiding (token) + +import Data + + +program :: Parser Code +program = + do { whiteSpace + ; ts <- tokenList + ; eof + ; return ts + } + +tokenList :: Parser Code +tokenList = many token <?> "list of tokens" + +token :: Parser GMLToken +token = + do { ts <- braces tokenList ; return (TBody ts) } + <|> do { ts <- brackets tokenList ; return (TArray ts) } + <|> (do { s <- gmlString ; return (TString s) } <?> "string") + <|> (do { t <- pident False ; return t } <?> "identifier") + <|> (do { char '/' -- No whitespace after slash + ; t <- pident True ; return t } <?> "binding identifier") + <|> (do { n <- number ; return n } <?> "number") + +pident :: Bool -> Parser GMLToken +pident rebind = + do { id <- ident + ; case (lookup id opTable) of + Nothing -> if rebind then return (TBind id) else return (TId id) + Just t -> if rebind then error ("Attempted rebinding of identifier " ++ id) else return t + } + +ident :: Parser String +ident = lexeme $ + do { l <- letter + ; ls <- many (satisfy (\x -> isAlphaNum x || x == '-' || x == '_')) + ; return (l:ls) + } + +gmlString :: Parser String +gmlString = lexeme $ between (char '"') (char '"') (many (satisfy (\x -> isPrint x && x /= '"'))) + +-- Tests for numbers +-- Hugs breaks on big exponents (> ~40) +test_number = "1234 -1234 1 -0 0" ++ + " 1234.5678 -1234.5678 1234.5678e12 1234.5678e-12 -1234.5678e-12" ++ + " -1234.5678e12 -1234.5678E-12 -1234.5678E12" ++ + " 1234e11 1234E33 -1234e33 1234e-33" ++ + " 123e 123.4e 123ee 123.4ee 123E 123.4E 123EE 123.4EE" + + +-- Always int or real +number :: Parser GMLToken +number = lexeme $ + do { s <- optSign + ; n <- decimal + ; do { string "." + ; m <- decimal + ; e <- option "" exponent' + ; return (TReal (read (s ++ n ++ "." ++ m ++ e))) -- FIXME: Handle error conditions + } + <|> do { e <- exponent' + ; return (TReal (read (s ++ n ++ ".0" ++ e))) + } + <|> do { return (TInt (read (s ++ n))) } + } + +exponent' :: Parser String +exponent' = try $ + do { e <- oneOf "eE" + ; s <- optSign + ; n <- decimal + ; return (e:s ++ n) + } + +decimal = many1 digit + +optSign :: Parser String +optSign = option "" (string "-") + + +------------------------------------------------------ +-- Library for tokenizing. + +braces p = between (symbol "{") (symbol "}") p +brackets p = between (symbol "[") (symbol "]") p + +symbol name = lexeme (string name) + +lexeme p = do{ x <- p; whiteSpace; return x } + +whiteSpace = skipMany (simpleSpace <|> oneLineComment <?> "") + where simpleSpace = skipMany1 (oneOf " \t\n\r\v") + oneLineComment = + do{ string "%" + ; skipMany (noneOf "\n\r\v") + ; return () + } + + +------------------------------------------------------------------------------ + +rayParse :: String -> Code +rayParse is = case (parse program "<stdin>" is) of + Left err -> error (show err) + Right x -> x + +rayParseF :: String -> IO Code +rayParseF file = + do { r <- parseFromFile program file + ; case r of + Left err -> error (show err) + Right x -> return x + } + +run :: String -> IO () +run is = case (parse program "" is) of + Left err -> print err + Right x -> print x + +runF :: IO () +runF = + do { r <- parseFromFile program "simple.gml" + ; case r of + Left err -> print err + Right x -> print x + } diff --git a/ghc/tests/programs/galois_raytrace/Pixmap.hs b/ghc/tests/programs/galois_raytrace/Pixmap.hs new file mode 100644 index 000000000000..11d20f0df2cd --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Pixmap.hs @@ -0,0 +1,64 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Pixmap where + +import Char +import IO hiding (try) +import Parsec + +readPPM f + = do h <- openFile f ReadMode + s <- hGetContents h + case (parse parsePPM f s) of + Left err -> error (show err) + Right x -> return x + +writePPM f ppm + = do h <- openFile f WriteMode + let s = showPPM (length (head ppm)) (length ppm) ppm + hPutStr h s + +-- parsing + +parsePPM + = do string "P6" + whiteSpace + width <- number + whiteSpace + height <- number + whiteSpace + colormax <- number + whiteSpace + cs <- getInput + return (chop width (chopColors cs)) + +chopColors [] = [] +chopColors (a:b:c:ds) = (ord a, ord b, ord c) : chopColors ds + +chop n [] = [] +chop n xs = h : chop n t + where (h, t) = splitAt n xs + +number + = do ds <- many1 digit + return (read ds :: Int) + +whiteSpace + = skipMany (simpleSpace <|> oneLineComment <?> "") + where simpleSpace = skipMany1 (oneOf " \t\n\r\v") + oneLineComment = + do char '#' + skipMany (noneOf "\n\r\v") + return () + +-- printing + +showPPM :: Int -> Int -> [[(Int,Int,Int)]] -> String +showPPM wid ht pss + = header ++ concat [[chr r, chr g, chr b] | ps <- pss, (r, g, b) <-ps] + where + header = "P6\n#Galois\n" ++ show wid ++ " " ++ show ht ++ "\n255\n" +showPPM _ _ _ = error "incorrect length of bitmap string" diff --git a/ghc/tests/programs/galois_raytrace/Primitives.hs b/ghc/tests/programs/galois_raytrace/Primitives.hs new file mode 100644 index 000000000000..2f2165405506 --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Primitives.hs @@ -0,0 +1,24 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Primitives where + +rad2deg :: Double -> Double +rad2deg r = r * 180 / pi + +deg2rad :: Double -> Double +deg2rad d = d * pi / 180 + +addi :: Int -> Int -> Int +addi = (+) + +addf :: Double -> Double -> Double +addf = (+) + +acosD :: Double -> Double +acosD x = acos x * 180 / pi + +asinD :: Double -> Double +asinD x = asin x * 180 / pi diff --git a/ghc/tests/programs/galois_raytrace/RayTrace.hs b/ghc/tests/programs/galois_raytrace/RayTrace.hs new file mode 100644 index 000000000000..cb15388e2c70 --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/RayTrace.hs @@ -0,0 +1,9 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module RayTrace(module Illumination, module Surface) where + +import Illumination +import Surface diff --git a/ghc/tests/programs/galois_raytrace/Surface.hs b/ghc/tests/programs/galois_raytrace/Surface.hs new file mode 100644 index 000000000000..832f0fcae270 --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/Surface.hs @@ -0,0 +1,115 @@ +-- Copyright (c) 2000 Galois Connections, Inc. +-- All rights reserved. This software is distributed as +-- free software under the license in the file "LICENSE", +-- which is included in the distribution. + +module Surface + ( SurfaceFn (..) + , Properties + , sfun, sconst + , prop + , matte, shiny + , chgColor + , surface + ) where + +import Geometry +import CSG +import Misc + +-- the surface gets passed face then u then v. +data SurfaceFn c v = SFun (Int -> Double -> Double -> Properties c v) + | SConst (Properties c v) + +sfun :: (Int -> Double -> Double -> Properties c v) -> SurfaceFn c v +sfun = SFun +sconst :: Properties c v -> SurfaceFn c v +sconst = SConst + +type Properties c v = (c, v, v, v) + +prop c d s p = (c, d, s, p) + +matte = (white, 1.0, 0.0, 1.0) +shiny = (white, 0.0, 1.0, 1.0) + +chgColor :: c -> Properties d v -> Properties c v +chgColor c (_, d, s, p) = (c, d, s, p) + +instance (Show c, Show v) => Show (SurfaceFn c v) where + show (SFun _) = "Surface function" + -- show (SConst p) = "Surface constant: " ++ show p + show (SConst p) = "Surface constant" + +evalSurface :: SurfaceFn Color Double -> Int -> Double -> Double -> Properties Color Double +evalSurface (SConst p) = \_ _ _ -> p +evalSurface (SFun f) = f + +-- calculate surface properties, given the type of +-- surface, and intersection point in object coordinates + +-- surface :: Surface SurfaceFn -> (Int, Point) -> (Vector, Properties) + +surface (Planar _ v0 v1) (n, p0, fn) + = (norm, evalSurface fn n' u v) + where norm = normalize $ cross v0 v1 + (n', u, v) = planarUV n p0 + +surface (Spherical _ v0 v1) (_, p0, fn) + = (norm, evalSurface fn 0 u v) + where x = xCoord p0 + y = yCoord p0 + z = zCoord p0 + k = sqrt (1 - sq y) + theta = adjustRadian (atan2 (x / k) (z / k)) + -- correct so that the image grows left-to-right + -- instead of right-to-left + u = 1.0 - clampf (theta / (2 * pi)) + v = clampf ((y + 1) / 2) + norm = normalize $ cross v0 v1 + +-- ZZ ignore the (incorrect) surface model, and estimate the normal +-- from the intersection in object space +surface (Cylindrical _ v0 v1) (_, p0, fn) + = (norm, evalSurface fn 0 u v) + where x = xCoord p0 + y = yCoord p0 + z = zCoord p0 + u = clampf $ adjustRadian (atan2 x z) / (2 * pi) + v = y + norm = normalize $ cross v0 v1 + +-- ZZ ignore the (incorrect) surface model, and estimate the normal +-- from the intersection in object space +surface (Conic _ v0 v1) (_, p0, fn) + = (norm, evalSurface fn 0 u v) + where x = xCoord p0 + y = yCoord p0 + z = zCoord p0 + u = clampf $ adjustRadian (atan2 (x / y) (z / y)) / (2 * pi) + v = y + norm = normalize $ cross v0 v1 + +planarUV face p0 + = case face of + PlaneFace -> (0, x, z) + + CubeFront -> (0, x, y) + CubeBack -> (1, x, y) + CubeLeft -> (2, z, y) + CubeRight -> (3, z, y) + CubeTop -> (4, x, z) + CubeBottom -> (5, x, z) + + CylinderTop -> (1, (x + 1) / 2, (z + 1) / 2) + CylinderBottom -> (2, (x + 1) / 2, (z + 1) / 2) + + ConeBase -> (1, (x + 1) / 2, (z + 1) / 2) + where x = xCoord p0 + y = yCoord p0 + z = zCoord p0 + +-- misc + +adjustRadian :: Radian -> Radian +adjustRadian r = if r > 0 then r else r + 2 * pi diff --git a/ghc/tests/programs/galois_raytrace/galois.gml b/ghc/tests/programs/galois_raytrace/galois.gml new file mode 100644 index 000000000000..5029d57620ef --- /dev/null +++ b/ghc/tests/programs/galois_raytrace/galois.gml @@ -0,0 +1,147 @@ + +[ [97 95 73 50 89 97 99 99 99 99 99 99 99 99 99 98 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 98 99 99 99 99 99 98 99 99 98 97 99 99 99 99 99 99 99 99 99 99 97 97 96 96 96 96 96 96 99 99 99] + [88 96 66 53 52 86 99 99 99 99 99 99 99 99 99 99 99 99 99 98 99 99 99 99 99 99 99 99 99 99 98 96 98 99 99 99 99 99 99 99 99 97 98 99 99 99 99 99 99 99 99 99 96 96 96 98 97 96 96 96 97 97 96] + [89 92 79 50 54 45 91 98 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 98 98 99 98 96 98 98 98 98 97 98 99 99 99 99 99 99 99 99 99 99 99 99 99 97 96 96 97 99 99 96 96 98 98 97 97] + [88 91 96 81 40 35 39 91 95 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 98 97 97 98 99 98 99 99 96 95 95 95 96 97 99 99 98 97 99 99 97 98 99 99 98 96 96 96 94 96 98 99 96 96 96 97 95 96] + [83 92 91 48 54 33 62 64 98 99 99 99 99 99 99 99 97 98 99 99 99 99 99 99 99 98 98 99 99 99 99 99 99 98 97 97 98 99 97 99 99 99 99 99 99 99 99 99 99 99 96 97 98 96 96 97 99 98 96 95 96 96 96] + [91 93 64 78 94 75 57 50 81 97 99 99 99 99 99 98 94 96 99 99 99 99 99 99 99 97 98 99 99 99 98 98 99 98 99 98 99 99 98 99 99 99 99 99 99 99 99 99 99 99 97 99 98 96 91 96 97 98 98 96 96 96 99] + [95 63 85 94 84 95 72 61 44 84 96 98 99 99 99 99 98 98 98 99 99 99 99 99 99 97 96 98 99 99 97 96 98 99 99 97 98 99 98 97 99 99 99 99 99 99 99 99 99 99 97 99 99 96 93 98 96 97 96 96 96 96 95] + [63 80 88 96 96 88 90 52 64 52 95 98 99 99 99 99 99 99 97 98 99 99 99 97 97 99 98 97 97 97 99 96 98 99 99 97 98 99 99 98 98 98 99 99 99 99 99 99 99 99 98 98 99 96 96 96 94 97 98 99 96 92 95] + [92 84 90 92 91 88 89 75 50 58 64 96 99 99 99 99 99 99 99 99 99 99 99 98 99 99 98 99 99 98 99 98 99 99 99 99 99 99 99 99 98 98 99 99 99 96 99 98 99 99 99 97 96 97 96 96 92 96 99 98 95 94 95] + [91 80 85 85 92 96 93 87 81 49 66 88 99 99 99 99 99 99 99 99 99 99 99 98 99 99 96 98 99 99 99 99 99 98 99 99 99 98 99 99 99 99 99 99 99 98 99 98 99 99 98 97 98 96 96 96 93 96 99 98 96 97 97] + [70 90 96 96 95 95 97 93 60 73 64 67 93 97 99 99 99 99 99 99 99 99 99 98 97 97 98 99 99 99 99 99 98 94 97 98 99 98 99 99 99 99 99 99 99 99 98 98 98 97 98 99 99 96 96 96 96 96 99 97 96 96 95] + [93 93 97 97 94 88 85 89 90 57 72 43 82 97 99 99 99 99 99 99 99 99 99 99 98 96 97 99 99 99 99 99 99 96 96 96 98 99 99 98 99 97 98 99 99 98 95 81 88 84 98 98 95 96 96 95 96 96 98 95 94 94 92] + [87 96 91 94 96 97 98 94 75 66 76 60 67 83 99 99 99 99 99 99 99 99 99 99 99 98 98 99 99 99 99 99 99 97 97 97 95 96 97 95 96 76 70 66 73 83 92 60 88 58 88 95 95 95 96 94 95 96 98 96 97 97 97] + [90 96 86 84 89 85 93 92 96 96 94 84 56 85 98 98 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 98 99 97 56 40 57 71 69 66 78 73 84 55 34 39 39 41 44 46 31 61 90 98 97 92 94 98 99 97 98 98 98] + [93 91 94 89 66 81 86 94 89 87 97 82 84 65 84 82 87 89 97 96 97 99 99 99 99 99 99 99 99 99 99 97 96 40 4 15 9 9 6 2 7 14 23 9 8 8 10 3 3 8 13 12 17 42 90 93 86 93 96 97 96 96 96] + [85 82 82 76 91 90 86 83 84 86 54 37 26 49 31 20 13 43 40 55 80 95 98 98 99 99 99 99 99 98 99 97 66 10 3 4 12 19 4 4 7 18 7 4 11 11 6 11 24 11 12 8 18 9 30 85 93 90 98 99 96 93 96] + [77 68 78 66 72 66 79 92 73 57 57 73 69 39 73 68 70 57 22 15 17 21 50 83 93 97 98 99 99 97 94 66 12 0 0 0 3 4 6 21 7 3 5 2 3 14 17 7 4 6 28 39 39 24 20 53 95 80 98 99 98 96 97] + [69 75 86 82 89 86 64 51 66 93 93 80 90 92 89 87 92 94 68 34 24 13 17 6 20 55 91 98 98 95 66 10 18 4 3 3 1 3 12 10 9 11 17 3 15 14 17 23 15 26 25 25 21 43 21 10 65 62 97 98 99 98 96] + [95 94 94 68 45 48 41 69 84 72 80 94 78 78 94 96 88 93 95 94 83 67 35 13 28 38 15 40 87 80 17 7 4 7 6 9 37 20 19 19 22 9 5 7 11 13 26 14 29 27 41 35 44 19 20 16 43 70 96 96 97 99 99] + [93 96 88 54 57 57 75 74 70 79 94 96 94 97 97 88 88 96 83 97 97 72 65 75 46 21 17 9 28 29 19 22 4 9 8 5 24 34 17 19 7 8 25 16 9 5 8 12 15 32 54 43 30 14 18 11 45 62 96 96 96 97 98] + [97 91 56 59 44 70 63 78 88 82 73 82 96 97 78 94 94 96 99 99 98 91 54 21 4 4 3 11 14 35 14 17 7 26 36 21 11 35 42 24 3 9 17 13 9 20 9 21 20 45 28 35 42 19 38 14 36 67 98 98 97 97 96] + [80 42 56 39 41 71 74 62 54 57 62 68 96 87 92 95 93 97 98 98 67 21 7 4 5 19 22 25 10 8 24 7 6 26 42 43 38 42 32 13 36 60 81 88 67 62 37 63 28 15 26 44 32 30 25 38 62 70 73 78 87 95 97] + [33 60 39 44 80 73 40 59 58 55 70 71 90 91 86 97 98 98 92 43 8 9 2 6 42 65 50 45 27 27 24 9 13 37 66 66 88 80 60 72 92 90 96 98 94 84 43 43 18 31 34 35 19 53 28 46 31 23 49 56 40 78 96] + [74 32 63 76 67 52 85 93 81 74 85 83 65 80 88 96 98 68 30 4 2 2 2 24 82 56 40 25 44 44 30 52 70 83 87 91 90 73 69 51 86 97 99 99 97 92 44 39 28 26 26 22 49 23 36 67 37 17 23 27 39 54 91] + [44 62 77 88 81 94 95 94 55 78 57 42 77 79 90 92 39 7 3 9 1 4 19 68 57 39 49 54 62 53 74 92 88 97 99 99 90 64 82 53 84 99 99 99 97 99 87 76 52 39 48 37 35 26 51 32 8 6 19 6 42 78 84] + [61 82 82 81 96 90 94 66 64 77 65 81 96 97 81 20 2 5 9 10 21 23 61 67 55 48 53 61 48 87 97 96 98 98 98 96 69 50 85 60 65 98 99 99 98 97 94 62 45 50 57 54 21 11 16 11 6 11 21 6 26 71 75] + [93 77 71 97 90 93 51 85 65 77 94 96 94 77 15 1 0 0 8 9 8 53 73 61 64 59 59 53 87 98 98 98 96 94 98 75 37 34 84 51 37 97 98 99 99 97 57 5 5 4 5 4 7 3 18 4 6 15 9 4 8 64 76] + [96 82 93 79 95 82 81 79 85 96 91 74 44 11 12 9 2 0 5 5 24 69 55 63 84 82 75 78 92 98 99 97 95 91 89 33 38 41 70 55 30 94 98 99 98 90 11 8 4 6 3 12 41 2 3 1 6 6 6 6 16 30 58] + [89 66 92 84 96 98 97 92 96 75 45 29 9 38 6 10 3 10 1 8 69 67 64 70 85 87 93 94 97 99 99 99 94 94 70 44 23 60 78 55 28 98 99 99 99 78 6 9 16 15 5 2 8 4 7 1 18 13 18 16 4 23 66] + [48 79 85 82 90 97 96 95 64 20 25 26 27 12 5 8 6 7 6 42 74 64 49 80 94 96 98 99 99 99 99 98 94 96 71 50 7 55 92 31 49 98 99 99 98 62 7 10 13 8 12 11 9 3 5 4 16 9 11 9 13 44 68] + [93 72 87 92 96 98 95 62 38 9 23 64 5 6 12 32 7 9 15 70 69 54 75 97 96 97 99 99 99 99 99 98 98 96 71 79 62 66 69 20 41 98 99 99 98 63 9 29 38 33 28 17 17 40 10 9 18 10 22 10 8 39 74] + [93 69 84 88 95 93 72 41 17 31 69 54 35 6 6 26 18 36 39 68 77 82 95 98 98 97 99 98 99 99 99 99 99 97 91 89 92 63 31 11 44 98 99 99 98 77 19 8 13 22 30 28 35 14 18 21 15 7 37 46 23 53 91] + [77 85 92 96 88 65 24 23 58 63 44 35 24 17 9 24 18 21 43 54 87 95 97 98 98 97 99 97 96 98 99 99 99 99 97 89 64 47 42 16 68 99 99 99 99 94 31 17 37 25 20 51 13 7 8 7 6 34 40 39 65 63 94] + [58 83 97 92 90 42 34 66 76 38 42 35 22 12 10 51 8 33 54 80 98 96 94 90 56 88 94 95 70 68 98 98 98 97 96 94 89 66 30 48 97 99 99 99 98 98 61 13 24 10 20 19 18 6 15 10 11 25 43 27 63 53 92] + [87 71 66 73 79 65 69 44 74 47 44 38 18 15 24 29 9 38 67 93 98 96 79 53 46 96 95 88 23 61 87 95 95 95 87 79 56 35 46 89 99 99 99 99 99 98 67 23 13 10 21 49 31 21 26 16 17 36 46 40 46 67 96] + [96 80 47 69 60 77 59 22 29 74 61 50 56 41 34 27 12 48 74 96 99 97 56 40 23 95 96 63 39 77 85 82 77 75 69 68 71 77 90 97 99 99 99 99 99 98 66 29 44 17 7 23 23 18 24 27 16 33 67 18 30 87 96] + [97 96 92 70 45 52 21 22 30 18 38 74 82 25 24 21 12 35 74 96 99 93 31 57 33 95 94 35 17 53 50 48 77 89 93 96 96 98 95 97 99 99 99 99 99 99 72 7 16 28 22 31 14 10 20 23 16 13 39 25 54 95 92] + [90 64 52 70 92 83 73 49 25 8 36 40 44 25 39 28 42 26 72 96 98 96 28 41 43 95 88 18 8 75 92 89 91 95 98 93 75 61 94 97 99 99 99 99 99 98 82 13 12 20 34 25 9 23 73 55 45 19 21 15 93 95 96] + [22 38 82 96 93 95 93 81 89 51 45 46 26 36 28 24 36 15 31 94 98 95 55 44 30 80 93 34 19 87 88 89 83 94 94 79 30 16 33 90 97 99 99 99 98 98 74 26 9 32 38 21 14 54 74 41 46 46 18 29 91 92 96] + [79 92 98 98 98 95 86 82 85 91 94 81 47 28 13 16 47 7 6 69 93 95 92 70 40 75 95 83 44 42 43 83 94 96 81 61 26 26 7 32 85 98 99 99 98 95 62 38 8 13 20 30 23 62 42 31 3 11 11 59 94 97 97] + [96 96 97 98 98 85 82 94 93 88 89 96 94 92 59 40 19 12 3 10 37 84 90 94 79 60 93 67 75 65 68 94 97 96 66 40 9 11 48 4 35 93 98 99 97 62 50 36 4 3 15 53 33 63 28 6 2 15 56 72 97 97 95] + [98 94 97 95 86 94 91 89 90 95 95 96 95 93 98 88 75 58 34 29 7 21 54 83 84 89 75 79 85 94 94 95 98 82 43 25 6 21 60 24 15 89 97 99 70 66 47 48 2 0 3 22 21 7 4 1 6 55 83 96 98 95 93] + [93 96 96 91 60 65 73 87 93 96 95 91 91 88 94 97 97 97 90 76 74 25 2 19 37 59 72 71 83 91 87 90 92 95 67 47 69 37 51 45 3 76 98 98 46 83 71 21 3 4 2 19 13 2 0 7 60 89 97 99 98 92 90] + [90 88 92 95 97 95 95 93 87 88 82 86 95 93 92 91 97 96 49 73 58 58 24 6 10 58 77 49 56 84 82 89 87 94 87 46 71 37 47 67 9 54 98 98 56 50 58 13 14 18 0 10 20 4 2 31 96 96 99 99 99 96 95] + [78 55 84 95 97 89 73 71 89 92 91 89 90 90 83 82 93 93 68 54 41 27 24 49 85 96 97 89 72 70 48 70 76 91 95 92 43 25 40 87 27 69 97 96 75 33 13 12 10 5 2 14 6 3 15 86 99 97 99 99 97 96 97] + [43 61 66 70 93 79 86 94 89 84 80 84 90 96 90 92 94 92 81 37 34 70 94 96 99 98 99 98 96 95 85 77 57 62 63 59 55 50 52 77 33 63 91 71 18 6 4 5 4 4 4 12 4 15 77 98 99 99 99 98 95 95 96] + [77 82 75 56 62 67 86 85 84 89 92 90 80 77 52 61 56 59 70 89 98 98 99 99 99 99 99 99 99 98 98 98 92 92 91 69 61 47 28 22 4 9 14 7 4 3 7 11 4 9 4 4 22 79 97 96 97 99 98 96 93 95 96] + [97 96 93 97 94 86 69 64 73 59 71 71 71 64 87 94 95 96 99 99 99 99 99 99 99 97 99 99 99 99 99 99 98 96 96 96 96 95 96 93 77 35 11 4 10 2 7 6 2 1 18 59 97 97 99 97 97 99 99 97 95 95 95] + [96 97 98 94 97 95 89 75 65 85 89 93 97 98 99 98 98 99 99 99 99 99 99 97 97 99 98 99 99 97 96 96 95 94 95 97 96 96 96 97 98 93 75 45 19 4 2 4 18 49 90 98 99 99 99 95 96 98 96 96 96 95 96] + [96 98 99 97 96 95 78 59 78 97 98 98 99 99 99 99 99 99 99 99 99 99 99 98 98 99 97 97 98 96 92 91 92 95 96 98 98 95 96 97 99 98 96 97 93 82 80 87 98 97 98 99 99 98 97 96 96 98 96 96 96 97 96] + [95 84 92 97 95 94 50 80 95 98 99 99 99 99 99 97 96 99 98 97 98 99 99 97 97 99 97 97 96 96 90 82 89 92 97 94 94 94 93 96 99 99 98 99 97 99 98 99 99 97 97 97 98 98 96 97 96 96 96 96 96 98 96] + [97 90 92 98 92 52 39 93 98 99 99 99 99 99 98 95 95 98 98 96 97 99 99 97 97 99 99 98 98 95 93 89 87 92 94 88 90 90 88 94 96 97 98 98 97 99 99 99 99 97 97 96 95 96 95 96 96 97 98 98 99 98 96] + [96 94 90 95 69 43 75 97 99 99 99 99 99 99 97 96 94 94 95 96 99 99 97 94 95 98 99 97 95 95 98 95 87 93 89 89 87 88 93 94 90 94 96 96 98 98 99 98 99 98 96 97 97 95 93 95 96 98 99 99 99 98 99] + [97 81 85 68 51 53 96 97 99 99 99 99 99 99 99 97 94 95 95 96 96 98 98 97 95 94 95 95 84 90 96 95 93 95 94 88 79 88 92 97 95 95 95 96 98 99 97 97 99 99 99 98 95 94 96 98 97 99 98 98 98 99 99] + [81 86 70 56 71 72 97 99 99 99 99 99 99 99 99 99 97 98 97 96 96 96 98 98 95 94 96 96 92 94 92 94 95 94 93 91 91 93 95 97 92 87 93 95 98 99 95 95 99 98 98 98 94 88 95 95 96 98 96 97 97 96 98] + [67 62 30 43 90 73 95 98 98 96 98 98 97 98 95 98 95 95 96 95 98 94 95 96 94 95 96 95 95 96 90 94 96 96 96 92 91 92 94 96 97 91 91 95 94 97 95 93 98 96 96 96 97 95 96 93 95 96 96 96 97 95 96] + [53 35 46 37 71 72 94 97 99 99 98 97 98 98 95 94 96 97 96 96 98 87 92 93 96 95 95 92 93 96 95 93 93 89 94 90 87 89 92 90 95 96 97 90 90 96 95 94 97 97 96 96 96 97 97 94 95 96 96 97 96 97 96] + [74 88 56 35 91 90 88 94 97 97 95 95 97 98 95 94 96 97 96 96 98 96 95 95 92 88 91 92 92 94 93 94 90 86 88 95 94 93 93 92 97 96 96 94 95 96 96 96 98 99 98 99 98 98 98 97 95 97 98 98 96 94 93] + [92 93 63 65 92 97 93 95 99 96 89 98 99 98 96 98 96 93 93 95 97 94 91 94 97 94 95 96 93 88 85 90 92 86 90 97 96 96 96 93 94 97 95 96 94 96 99 96 96 96 96 98 98 96 95 95 94 97 96 93 92 95 93] + [82 94 95 81 92 95 92 97 99 98 94 93 92 97 94 97 97 98 94 90 97 95 92 94 94 90 91 96 92 88 94 94 89 83 90 97 96 96 95 96 97 95 94 97 96 96 97 94 93 93 96 98 94 96 96 92 89 90 94 94 94 92 88] + [78 85 76 94 97 95 96 97 99 99 98 96 97 97 96 97 97 95 95 96 98 96 96 96 88 86 93 96 94 93 89 88 88 90 90 94 94 97 97 96 96 97 98 98 98 96 92 87 96 96 96 96 96 94 92 93 88 88 93 95 94 90 87] + [83 88 91 94 97 97 99 98 98 98 98 99 99 98 99 98 99 97 98 97 98 96 96 95 96 94 96 95 95 91 85 90 90 93 94 94 92 94 95 96 98 97 98 98 97 97 97 96 96 96 95 96 94 95 95 94 93 93 94 95 93 88 91] + [95 90 94 94 98 96 98 99 96 98 97 97 98 98 99 99 96 95 97 97 99 95 96 98 93 96 96 96 93 95 89 93 93 95 96 96 97 97 97 97 99 98 97 96 98 99 99 99 98 93 93 96 96 96 95 94 91 92 90 93 94 96 96] +] /galois + +{ /v /u /face % bind parameters + { % toIntCoord : float -> int + 63.0 mulf floor /i % i = floor(3.0*i) + i 63 eqi { 62 } { i } if % return max(2, i) + } /toIntCoord + galois u toIntCoord apply get % val = texture[u][v] + v toIntCoord apply get + real 100.0 divf /gal + gal gal gal point % b/w galois + 1.0 % kd = 1.0 + 0.0 % ks = 0.0 + 1.0 % n = 1.0 +} /galoisface + + +galoisface cube +-0.5 -0.5 -0.5 translate % center +2.5 uscale % make it bigger +-25.0 rotatex -25.0 rotatey % rotate +0.0 -1.0 7.0 translate % move to final position + +%galoisface cylinder +%-0.5 -0.5 -0.5 translate % center +%1.5 uscale % make it bigger +%0.0 rotatex 90.0 rotatey % rotate +%0.0 0.0 5.0 translate % move to final position + +%galoisface sphere +%-0.5 -0.5 -0.5 translate % center +%1.5 uscale % make it bigger +%-25.0 rotatex 25.0 rotatey % rotate +%-3.0 0.0 5.0 translate % move to final position + +%union union % join the 3 together + +{ /v /u /face + v 5.0 divf /v + u 5.0 divf /u + v floor 2 modi 0 eqi + { 1.0 } + { 0.8 } + if /r + u floor 2 modi 0 eqi + { 1.0 } + { 0.8 } + if /g + v frac /v + u frac /u + v 0.0 lessf { v 1.0 addf } { v } if /v + u 0.0 lessf { u 1.0 addf } { u } if /u + { % toIntCoord : float -> int + 63.0 mulf floor /i % i = floor(3.0*i) + i 63 eqi { 62 } { i } if % return max(2, i) + } /toIntCoord + galois u toIntCoord apply get % val = texture[u][v] + v toIntCoord apply get + real 100.0 divf /gal + r gal mulf g gal mulf gal point % b/w galois + 0.0 % kd = 1.0 + 1.0 % ks = 0.0 + 1.0 % n = 1.0 +} plane /p + +p 0.0 -3.0 0.0 translate % plane at Y = -3 + +union + +/scene + % directional light +1.0 -1.0 0.0 point % direction +1.0 1.0 1.0 point light /l % directional light + +1.0 0.5 0.0 point % ambient light +[ l ] % lights +scene % scene to render +3 % tracing depth +90.0 % field of view +300 200 +"galois.ppm" % output file +render + diff --git a/ghc/tests/programs/galois_raytrace/galois_raytrace.stdout b/ghc/tests/programs/galois_raytrace/galois_raytrace.stdout new file mode 100644 index 0000000000000000000000000000000000000000..6cbbcb5457719bd39b4b8af033c0357da214e0ef GIT binary patch literal 180024 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 Ts)%8yBxVKdl*Byc@Du+5pBqoI literal 0 HcmV?d00001 -- GitLab