Full Code of flannelhead/blackstar for AI

master 40f1f1965292 cached
26 files
43.3 KB
12.3k tokens
1 requests
Download .txt
Repository: flannelhead/blackstar
Branch: master
Commit: 40f1f1965292
Files: 26
Total size: 43.3 KB

Directory structure:
gitextract_5lpryipi/

├── .gitignore
├── LICENSE
├── README.md
├── Setup.hs
├── animations/
│   └── default-ani.yaml
├── app/
│   ├── Animate.hs
│   ├── GenerateTree.hs
│   └── Main.hs
├── blackstar.cabal
├── scenes/
│   ├── closeup.yaml
│   ├── default-aa.yaml
│   ├── default.yaml
│   ├── fartheraway.yaml
│   ├── lensing-disk.yaml
│   ├── lensing.yaml
│   ├── wideangle-disk.yaml
│   ├── wideangle.yaml
│   └── wideangle1.yaml
├── scripts/
│   └── ffmpeg-animate
├── src/
│   ├── Animation.hs
│   ├── ConfigFile.hs
│   ├── ImageFilters.hs
│   ├── Raytracer.hs
│   ├── StarMap.hs
│   └── Util.hs
└── stack.yaml

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.stack-work
blackstar.prof
out.png
texture.jpg
BSC5
SAO.pc
PPM
PPMra
renders
bloomed.png
output
*.png
!example.png
frames*
*.mkv
dist*
.ghc.*



================================================
FILE: LICENSE
================================================
Copyright Sakari Kapanen (c) 2016

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.

    * Neither the name of Author name here nor the names of other
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: README.md
================================================
# Blackstar
A black hole ray tracer written in Haskell. There's [an article](https://flannelhead.github.io/projects/blackstar.html) about this on my homepage. I've also written a [theoretical writeup](https://flannelhead.github.io/posts/2016-03-06-photons-and-black-holes.html) on Schwarzschild geodesics.

![An example image](https://raw.githubusercontent.com/flannelhead/blackstar/master/example.png)

## Features
* Fast, parallel ray tracing
* Rendering [Schwarzschild](https://en.wikipedia.org/wiki/Schwarzschild_metric) black holes
* Rendering accretion disks
* Drawing the celestial sphere using a star catalogue
* Bloom effect
* Antialiasing by 4x supersampling for smoother images
* Easy, YAML based configuration
* A simple CLI
* Batch mode and sequence generator for creating animations

## What about the name?
It is a tribute to David Bowie, referring to his last album.

## Building
Use [`stack`](http://docs.haskellstack.org/en/stable/README/) to build this. First clone the repo, then run `stack build` and follow the instructions given by `stack`. You should be able to build `blackstar` on any platform where you can install `stack`.

You will have to build the star lookup tree first. Download the [PPM star catalog](http://tdc-www.harvard.edu/software/catalogs/ppm.html) [this archive](http://tdc-www.harvard.edu/software/catalogs/ppm.tar.gz) and extract the file `PPM` to the root folder of this project. Then run `stack exec generate-tree PPM stars.kdt` and the tree will be generated and saved to the file `stars.kdt`.

### Speeding it up with LLVM
When doing large or batch renders, it is recommended to build `blackstar` using GHC's LLVM backend. GHC produces LLVM bytecode and LLVM produces fast native code from GHC's output. In my tests I've noticed ~1.5x speedups.

The LLVM backend isn't used by default since one needs to install (and usually build) a specific version of LLVM separately. Moreover, the build time is significantly higher with LLVM, so one doesn't definitely want to use it while hacking on the code.

To successfully build with LLVM, you need to:

* Download and [build](http://llvm.org/docs/GettingStarted.html#getting-started-quickly-a-summary) [LLVM 6.0.1](http://llvm.org/releases/download.html#6.0.1). You can skip the Clang parts. After the build, you should make sure the tools `llc` and `opt` are found in your `PATH`. Notice that these aren't included in the prebuilt LLVM binaries, that's why you'll need to build it.
* Build `blackstar` with `stack build --ghc-options -fllvm`. (If you've just built it, run `stack clean` first to ensure it really gets rebuilt with LLVM.)
* Wait patiently
* Enjoy the result!

You don't necessarily have to use LLVM at all. However, if you can acquire binaries of the right LLVM version, that will give you some speedups.

## Usage
When `blackstar` has been built with `stack`, you can run it with
```
stack exec blackstar -- [-p|--preview] [-f|--force] [-o|--output=PATH] [-s|--starmap=PATH] SCENENAME
```
Notice the two dashes (`--`) which are required to terminate `stack`'s argument list.


`cabal` users can run `blackstar` by executing
```
cabal run -- [OPTIONS] SCENENAME
```
in the root folder of the project.

Scenes are defined using YAML config files. Look in the `scenes` folder for examples. To render the `default` scene to the directory `output`, run
```
stack exec blackstar -- scenes/default.yaml --output output
```
in the root directory of the project. The `--output` flag specifies the output directory. By default, `blackstar` searches for a starmap in the path `./stars.kdt`, but a different path can be specified using the `--starmap` flag.

The rendered files are named `scenename.png` and `scenename-bloomed.png`. The `--preview` flag can be used to render small-sized previews of the scene while adjusting the parameters. The `--force` flag will cause `blackstar` to overwrite output images without a prompt.

If a directory is given as the input scene path, `blackstar` searches non-recursively for YAML files in that directory and tries to render them. The scenes are placed in the specified output directory.

There's also a help text which can be seen by running
```
stack exec blackstar -- --help
OR
cabal run -- --help
```

Better images can be achieved by rendering larger than the target size and then scaling down (some antialiasing is achieved). This is called supersampling and is implemented in `blackstar`. It can be enabled by setting `supersampling` to true in the YAML config file — see `scenes/default-aa.yaml` for an example.

## Animation
There is a separate YAML config format for specifying animations. For example, see [default-ani.yaml](animations/default-ani.yaml).

In the first pass, the animation file must be rendered into separate config files for each frame. The `animate` executable takes care of this. First, create a directory where the frame config files will be put.
```
mkdir frames
```
Then run `animate`:
```
stack exec animate -- animations/default-ani.yaml -o frames
```
Now you should find quite a bunch of `.yaml` files in the folder `frames`.

Make another folder for the output frames:
```
mkdir frames-out
```
Now you will be able to run `blackstar` in batch mode to render the frames:
```
stack exec blackstar -- frames -o frames-out
```
This will take quite a while.

After the frames have been rendered, generate a video from the `*.png` still with your utility of preference. You can also use my script `scripts/ffmpeg-animate`, which uses `ffmpeg`. You only need to give it the prefix of the numbered frames:
```
scripts/ffmpeg-animate frames-out/default-ani
```
The output video will be rendered to `out.mkv`.

## Profiling
Thanks to `stack`, profiling is incredibly easy. Rebuild `blackstar` by running
```
stack build --profile
```
and then run it with
```
stack exec blackstar -- scenes/default.yaml -o output +RTS -p
```
The profile will be generated to `blackstar.prof`.

## TODO
As always, there's a plenty of room for improvement. For example:

* Animation: mathematically rigorous non-stationary observer
* Arbitrary textures for accretion disk (or some cool noise generator)
* Redshifting of the accretion disk
* Preview / scene planner GUI ([fltkhs](https://hackage.haskell.org/package/fltkhs))

Pull requests are welcome! If you find some cool scenes, I'd appreciate if you contributed them to this repository.



================================================
FILE: Setup.hs
================================================
import Distribution.Simple
main = defaultMain


================================================
FILE: animations/default-ani.yaml
================================================
# Animation files are quite similar to scene files. The difference is that you
# can define multiple camera instances which are then fixed to certain points
# of time (normalized to [0, 1[). The frames in between are then generated by
# interpolating between the keyframes using the interpolation method of choice.

# Scene config is similar to the scene files
scene:
    resolution: [1920, 1080]
    bloomStrength: 0.7
    starIntensity: 0.7
    starSaturation: 0.7
    diskHSV: [180, 0.1, 1.05]
    diskOpacity: 0.95
    diskInner: 1.8
    diskOuter: 13
    supersampling: true

# The number of frames in the animation
nFrames: 375
# Interpolation method. Currently, 'linear' is the only option
interpolation: 'linear'

# Specify any number of keyframes you want
keyframes:
- time: 0  # The normalize time of the keyframe. 0 is the first frame, 1 is the last
  camera:  # Camera config is similar to the scene files
    position:   [3, 3, -20]
    lookAt:     [-7, 5, 0]
    upVec:      [-0.2, 1, 0]
    fov:        1.5
- time: 1
  camera:
    position:   [-15, 1, -20]
    lookAt:     [13, -7, 0]
    upVec:      [-0.2, 1, 0]
    fov:        2


================================================
FILE: app/Animate.hs
================================================
{-# LANGUAGE DeriveDataTypeable #-}
{-# OPTIONS_GHC -fno-cse #-}

module Main where

import Prelude hiding (writeFile)
import System.Directory
import System.FilePath
import Data.Yaml (decodeFileEither, prettyPrintParseException, encode)
import System.Console.CmdArgs
import Control.Monad (forM_)
import Data.ByteString.Lazy (fromStrict, writeFile)

import Animation
import Util

data Animate = Animate { inFile :: FilePath
                       , output :: FilePath
                       , force :: Bool }
                       deriving (Show, Data, Typeable)

argparser :: Animate
argparser = Animate { inFile = def
                            &= argPos 0
                            &= typ "INPUTFILE"
                    , output = ""
                            &= help "output directory"
                            &= typ "PATH"
                    , force  = def
                            &= help "overwrite images without asking" }
                    &= summary "Animation helper for Blackstar"

main :: IO ()
main = do
    cmdline <- cmdArgs argparser

    let inPath = inFile cmdline
    let basename = takeBaseName inPath
    inputExists <- doesFileExist inPath

    outPath <- normalizePath =<< case output cmdline of
                  "" -> getCurrentDirectory
                  x  -> return x
    createDirectoryIfMissing True outPath

    if inputExists then do
        config <- decodeFileEither inPath
        case config of
            Right cfg ->
                case validateKeyframes $ keyframes cfg of
                    Right () -> do
                        let nFr = nFrames cfg
                        forM_ (zip (generateFrames cfg) [(0 :: Int), 1 ..])
                            (\(frame, idx) -> do
                                let filename = outPath </> basename ++ "_" ++
                                        padZero (nFr - 1) idx <.> ".yaml"
                                let outBl = fromStrict $ encode frame
                                if force cmdline
                                    then writeFile filename outBl
                                    else promptOverwriteFile filename 
                                           (\fname -> writeFile fname outBl)
                            )
                    Left err -> putStrLn err
            Left err -> putStrLn $ "Error when decoding config:\n" ++
                                   prettyPrintParseException err
        else putStrLn "Couldn't open input file."


================================================
FILE: app/GenerateTree.hs
================================================
module Main where

import qualified Data.ByteString.Lazy as B
import System.Environment (getArgs)

import Util
import StarMap

-- Generate and store the k-d star tree from a star catalog

main :: IO ()
main = do
    args <- getArgs
    case args of
        [infile, outfile] -> do
            outfile' <- normalizePath outfile
            infile' <- normalizePath infile
            eitherMap <- readMapFromFile infile'
            case eitherMap of
                Right stars -> do
                    putStrLn "Generating the star tree..."
                    tree <- timeAction "Building the tree"
                        $ buildStarTree stars
                    let treeBl = B.fromStrict $ treeToByteString tree
                    promptOverwriteFile outfile'
                        (\filename -> B.writeFile filename treeBl)
                    putStrLn $ "Tree saved to " ++ outfile' ++ "."
                Left  err   ->  putStrLn err
        _ -> putStrLn "Usage: generate-tree <INFILE> <OUTFILE>"


================================================
FILE: app/Main.hs
================================================
{-# LANGUAGE DeriveDataTypeable #-}
{-# OPTIONS_GHC -fno-cse #-}

module Main where

import System.Directory
import Control.Monad (when, forM_)
import Data.Yaml (decodeFileEither, prettyPrintParseException)
import System.Console.CmdArgs
import System.FilePath (takeBaseName, takeExtension, (</>), (<.>))
import Data.List (sort)
import System.Console.ANSI (clearScreen, setCursorPosition)

import Raytracer
import StarMap
import ConfigFile
import ImageFilters
import Util

data Blackstar = Blackstar { preview :: Bool
                           , output :: String
                           , force :: Bool
                           , starmap :: String
                           , inputfile :: String }
                           deriving (Show, Data, Typeable)

argparser :: Blackstar
argparser = Blackstar { preview = def
                          &= help "preview render (small size)"
                      , output = ""
                          &= help "output directory"
                          &= typ "PATH"
                      , force = def
                          &= help "overwrite images without asking"
                      , starmap = "stars.kdt"
                          &= help "path to starmap"
                          &= typ "PATH"
                      , inputfile = def
                          &= argPos 0
                          &= typ "INPUTFILE"
                      } &= summary "Blackstar v0.1"

main :: IO ()
main = do
    cmdline <- cmdArgs argparser
    etree <- readTreeFromFile $ starmap cmdline
    case etree of
        Right tree -> putStrLn "Starmap successfully read."
            >> doStart cmdline tree
        Left  err  -> putStrLn $ "Error decoding star tree: \n" ++ err

doStart :: Blackstar -> StarTree -> IO ()
doStart cmdline tree = do
    -- Resolve the output directory
    when (output cmdline /= "")
        $ createDirectoryIfMissing True (output cmdline)
    outdir <- normalizePath =<< case output cmdline of
                  "" -> getCurrentDirectory
                  x  -> return x
    createDirectoryIfMissing True outdir
    -- Resolve the input file or directory
    filename <- normalizePath $ inputfile cmdline
    isDir <- doesDirectoryExist filename
    if isDir then do
            putStrLn $ filename
                ++ " is a directory. Rendering all scenes inside it..."

            inputFiles <- map (filename </>)
                . sort . filter (\f -> takeExtension f == ".yaml")
                <$> getDirectoryContents filename

            forM_ (zip inputFiles [(1 :: Int)..]) $ \(scn, idx) -> do
                clearScreen
                setCursorPosition 0 0
                putStrLn $ "Batch mode progress: " ++ show idx ++ "/"
                    ++ show (length inputFiles)
                handleScene cmdline tree outdir scn
        else handleScene cmdline tree outdir filename

handleScene :: Blackstar -> StarTree -> String -> String -> IO ()
handleScene cmdline tree outdir filename = do
    let pvw = preview cmdline
    let sceneName = takeBaseName filename
    putStrLn $ "Reading " ++ filename ++ "..."
    cfg <- decodeFileEither filename
    let sceneName' = if pvw then "prev-" ++ sceneName else sceneName
    case cfg of
        Right config -> putStrLn "Scene successfully read."
                          >> doRender cmdline (prepareScene config pvw) tree
                               sceneName' outdir
        Left  err    -> putStrLn $ prettyPrintParseException err

prepareScene :: Config -> Bool -> Config
prepareScene cfg doPreview = let
    scn = scene cfg
    (w, h) = resolution scn
    res = 300
    newRes = if w >= h then (res, res * h `div` w) else (res * w `div` h, res)
    newScn = if doPreview then scn { resolution = newRes
                                   , supersampling = False
                                   , bloomStrength = 0 }
                          else scn
    in cfg { scene = newScn }

doRender :: Blackstar -> Config -> StarTree -> String -> String -> IO ()
doRender cmdline cfg tree sceneName outdir = do
    putStrLn $ "Rendering " ++ sceneName ++ "..."
    let scn = scene cfg
    img <- timeAction "Rendering" $ render cfg tree

    let outName = outdir </> sceneName <.> ".png"

    final <- if bloomStrength scn /= 0
        then do
            putStrLn "Applying bloom..."
            bloomed <- bloom (bloomStrength scn) (bloomDivider scn) img
            timeAction "Bloom" bloomed
        else return img

    putStrLn $ "Saving to " ++ outName ++ "..."
    if force cmdline
      then writeImg final outName
      else promptOverwriteFile outName (writeImg final)

    putStrLn "Everything done. Thank you!"


================================================
FILE: blackstar.cabal
================================================
name:                blackstar
version:             0.1.0.0
synopsis:            A black hole ray tracer
description:         A black hole ray tracer written in Haskell
homepage:            http://github.com/flannelhead/blackstar#readme
license:             BSD3
license-file:        LICENSE
author:              Sakari Kapanen
maintainer:          sakari.m.kapanen@gmail.com
copyright:           2019 Sakari Kapanen
category:            Graphics
build-type:          Simple
-- extra-source-files:
cabal-version:       >=1.10

library
  hs-source-dirs:      src
  exposed-modules:     Raytracer
                     , StarMap
                     , ConfigFile
                     , ImageFilters
                     , Util
                     , Animation
  ghc-options:         -Wall
                       -O2
  build-depends:       base >= 4.7 && < 5
                     , data-default
                     , linear
                     , lens
                     , bytestring
                     , cereal
                     , vector
                     , kdt
                     , yaml
                     , aeson
                     , directory
                     , massiv
                     , massiv-io
                     , filepath
                     , time
                     , deepseq
  default-language:    Haskell2010

executable blackstar
  hs-source-dirs:      app
  main-is:             Main.hs
  ghc-options:         -Wall -rtsopts -threaded -with-rtsopts=-N
  build-depends:       base
                     , blackstar
                     , directory
                     , yaml
                     , bytestring
                     , cmdargs
                     , filepath
                     , ansi-terminal
  default-language:    Haskell2010

executable animate
  hs-source-dirs:      app
  main-is:             Animate.hs
  ghc-options:         -Wall
  build-depends:       base
                     , blackstar
                     , directory
                     , yaml
                     , bytestring
                     , cmdargs
                     , filepath
  default-language:    Haskell2010

executable generate-tree
  hs-source-dirs:      app
  main-is:             GenerateTree.hs
  ghc-options:         -Wall
  build-depends:       base
                     , blackstar
                     , directory
                     , bytestring
  default-language:    Haskell2010

source-repository head
  type:     git
  location: https://github.com/flannelhead/blackstar


================================================
FILE: scenes/closeup.yaml
================================================
camera:
    position:   [10, 1, -2]
    lookAt:     [0, 0, 6]
    upVec:      [0, 1, 0]
    fov:        1.2

scene:
    resolution: [1280, 960]
    bloomStrength: 0.7
    starIntensity: 0.7
    starSaturation: 0.7
    diskOpacity: 0.95
    diskInner: 3
    diskOuter: 9


================================================
FILE: scenes/default-aa.yaml
================================================
camera:
    position:   [0, 1, -20]
    lookAt:     [2, 0, 0]
    upVec:      [-0.2, 1, 0]
    fov:        1.5

scene:
    resolution: [1920, 1080]
    bloomStrength: 0.15
    starIntensity: 0.4
    starSaturation: 1.5
    diskColor: [180, 0.1, 1.05]
    diskOpacity: 0.95
    diskInner: 1.8
    diskOuter: 13
    supersampling: true


================================================
FILE: scenes/default.yaml
================================================
camera:
    # All the vectors are [x, y, z] coordinates
    position:   [0, 1, -20]  # The position of the camera
    lookAt:     [2, 0, 0]  # The point to look at
    upVec:      [-0.2, 1, 0]  # The "up" direction vector which determines the
                              # orientation of the camera
    fov:        1.5           # The tangent of the view angle

scene:
    resolution: [1920, 1080]   # [width, height] of the image
    bloomStrength: 0.15 # The strength (weight) of the bloom effect. Setting this to
                        # 0 disables it entirely
    bloomDivider: 25  # A number x such that r = image width / x is the bloom radius

    starIntensity: 0.4   # The intensity (0 = black, 1 = white) of the stars
    starSaturation: 1.5  # The color saturation of the stars

    diskOpacity: 0.95  # Opacity of the accretion disk (0 = fully transparent,
                       # 1 = fully opaque)
    diskInner: 1.8     # The inner radius of the accretion disk
    diskOuter: 13      # The outer radius of the accretion disk
    diskColor: [180, 0.1, 1.05]  # The colour of the accretion disk in the HSI color space.
                                 # H: 0..360, S: 0..1, I: 0..1

    supersampling: false  # Set this to true to enable smoothing by supersampling
                          # a 4x sized image
    stepSize: 0.3  # The size of the timestep in the simulation. Usually this value
                   # should be fine.


================================================
FILE: scenes/fartheraway.yaml
================================================
camera:
    position:   [-25, 1, -60]
    lookAt:     [-12, -4, 0]
    upVec:      [0.15, 1, 0]
    fov:        2

scene:
    resolution: [1920, 1080]
    bloomStrength: 0.15
    starIntensity: 0.4
    starSaturation: 1.5
    diskOpacity: 0.95
    diskInner: 3
    diskOuter: 12
    supersampling: true


================================================
FILE: scenes/lensing-disk.yaml
================================================
camera:
    position:   [30, 0.4, 3]
    lookAt:     [0, 0, 0]
    upVec:      [0, 1, 0.2]
    fov:        1

scene:
    resolution: [1280, 800]
    bloomStrength: 0.15
    starIntensity: 0.4
    starSaturation: 1.5
    diskOpacity: 0.95
    diskInner: 3
    diskOuter: 12
    supersampling: true


================================================
FILE: scenes/lensing.yaml
================================================
camera:
    position:   [30, 0.4, 3]
    lookAt:     [0, 0, 0]
    upVec:      [0, 1, 0.2]
    fov:        1

scene:
    resolution: [1600, 1200]
    bloomStrength: 0.15
    starIntensity: 0.4
    starSaturation: 1.5
    diskOpacity: 0
    diskInner: 3
    diskOuter: 12
    supersampling: true


================================================
FILE: scenes/wideangle-disk.yaml
================================================
camera:
    position:   [-6, 1, -20]
    lookAt:     [-6, -4, 0]
    upVec:      [-0.2, 1, 0]
    fov:        3.5

scene:
    resolution: [1920, 1080]
    bloomStrength: 0.15
    starIntensity: 0.4
    starSaturation: 1.5
    diskOpacity: 0.95
    diskInner: 2.5
    diskOuter: 12
    supersampling: true


================================================
FILE: scenes/wideangle.yaml
================================================
camera:
    position:   [20, 0, 0]
    lookAt:     [0, 0, 3.5]
    upVec:      [0, 1, 0]
    fov:        2

scene:
    resolution: [1920, 1020]
    bloomStrength: 0.15
    starIntensity: 0.4
    starSaturation: 1.5
    diskOpacity: 0
    supersampling: true


================================================
FILE: scenes/wideangle1.yaml
================================================
camera:
    position:   [0, 0, 20]
    lookAt:     [3.5, 0, 0]
    upVec:      [0, 1, 0]
    fov:        2

scene:
    resolution: [1920, 1080]
    bloomStrength: 0.15
    starIntensity: 0.4
    starSaturation: 1.5
    diskOpacity: 0
    supersampling: true


================================================
FILE: scripts/ffmpeg-animate
================================================
#!/bin/sh

# Create YouTube quality video from stills using ffmpeg
#
# USAGE:
# ffmpeg-animate PREFIX
#
# Writes to file "out.mkv" in the current directory

ffmpeg -f image2 -i "${1}_%03d.png" \
    -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p \
    -r 25 out.mkv



================================================
FILE: src/Animation.hs
================================================
{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveGeneric #-}

module Animation ( Keyframe(camera, time)
                 , Animation(scene, nFrames, interpolation, keyframes)
                 , InterpolationMethod(Linear)
                 , generateFrames, validateKeyframes ) where

import Data.List (sortBy)
import Data.Ord (comparing)
import qualified ConfigFile as CF
import Data.Aeson.Types
import Linear ((*^))
import GHC.Generics

data Keyframe = Keyframe { camera :: CF.Camera
                         , time :: Double }
                         deriving (Generic)

data InterpolationMethod = Linear

data Animation = Animation { scene :: CF.Scene
                           , nFrames :: Int
                           , interpolation :: InterpolationMethod
                           , keyframes :: [Keyframe] }
                           deriving (Generic)

instance FromJSON Keyframe

instance FromJSON InterpolationMethod where
    parseJSON str = do
        (str' :: String) <- parseJSON str
        return $ case str' of
            "linear" -> Linear
            _        -> Linear

instance FromJSON Animation

validateKeyframes :: [Keyframe] -> Either String ()
validateKeyframes []  = Left "Must have at least two keyframes"
validateKeyframes [_] = validateKeyframes []
validateKeyframes frs = if time (head frs) == 0 && time (last frs) == 1
    then Right ()
    else Left "First keyframe must have time == 0, last time == 1"

generateFrames :: Animation -> [CF.Config]
generateFrames animation = let
    stepsize = (1 :: Double) / fromIntegral (nFrames animation - 1)
    -- Take the first keyframe from the scene in the config
    -- Also sort the frames by time
    frames = sortBy (comparing time) $ keyframes animation
    points = (* stepsize) . fromIntegral <$> [0 .. nFrames animation - 1]
    in map (makeFrame animation frames) points

makeFrame :: Animation -> [Keyframe] -> Double -> CF.Config
makeFrame animation frames point = let
        scn = scene animation
        mtd = interpolation animation
    in CF.Config { CF.camera = interpolate mtd frames point
                 , CF.scene = scn }

interpolate :: InterpolationMethod -> [Keyframe] -> Double -> CF.Camera
interpolate method frames t = let
        findFrames (fr1 : fr2 : frs) = if t >= time fr1 && t < time fr2
            then (fr1, fr2)
            else findFrames (fr2 : frs)
        findFrames [fr] = (fr, fr { time = time fr + 1 } )

        (f1, f2) = findFrames frames
        t' = (t - time f1) / (time f2 - time f1)

        f :: Fractional a => (Double -> a -> a) -> a -> a -> a
        f = interpolationFunction method t'

        cam1 = camera f1
        cam2 = camera f2
    in CF.Camera { CF.fov = f (*) (CF.fov cam1) (CF.fov cam2)
                 , CF.position = f (*^) (CF.position cam1) (CF.position cam2)
                 , CF.lookAt = f (*^) (CF.lookAt cam1) (CF.lookAt cam2)
                 , CF.upVec = f (*^) (CF.upVec cam1) (CF.upVec cam2) }

interpolationFunction :: Fractional a => InterpolationMethod -> Double
                                      -> (Double -> a -> a)
                                      -> a -> a -> a
{-# INLINE interpolationFunction #-}
interpolationFunction method t times a b = case method of
    Linear -> a + t `times` (b - a)


================================================
FILE: src/ConfigFile.hs
================================================
{-# LANGUAGE OverloadedStrings, FlexibleInstances, DeriveGeneric #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module ConfigFile
    ( Scene( Scene, safeDistance, stepSize, bloomStrength, bloomDivider
           , starIntensity, starSaturation, supersampling
           , diskColor, diskOpacity, diskInner, diskOuter, resolution )
    , Camera( Camera, position, lookAt, upVec, fov )
    , Config( Config, camera, scene ) ) where

import Data.Aeson.Types
import Linear
import GHC.Generics
import Graphics.ColorSpace

data Config = Config { scene :: Scene
                     , camera :: Camera }
                     deriving (Generic)

data Scene = Scene { safeDistance :: !Double
                   , stepSize :: !Double
                   , bloomStrength :: !Double
                   , bloomDivider :: !Int
                   , starIntensity :: !Double
                   , starSaturation :: !Double
                   , diskColor :: !(Pixel HSI Double)
                   , diskOpacity :: !Double
                   , diskInner :: !Double
                   , diskOuter :: !Double
                   , resolution :: !(Int, Int)
                   , supersampling :: !Bool }
                   deriving (Generic)

data Camera = Camera { position :: !(V3 Double)
                     , lookAt :: !(V3 Double)
                     , upVec :: !(V3 Double)
                     , fov :: !Double }
                     deriving (Generic)

instance FromJSON (V3 Double) where
    parseJSON vec = do
        [x, y, z] <- parseJSON vec
        return $ V3 x y z

instance ToJSON (V3 Double) where
    toJSON (V3 x y z) = toJSON [x, y, z]

instance FromJSON (Pixel HSI Double) where
    parseJSON hsi = do
        [x, y, z] <- parseJSON hsi
        return $ PixelHSI (x / 360) y z

instance ToJSON (Pixel HSI Double) where
    toJSON (PixelHSI h s i) = toJSON [360 * h, s, i]

instance FromJSON Config

instance ToJSON Config where
    toEncoding = genericToEncoding defaultOptions

instance FromJSON Camera

instance ToJSON Camera where
    toEncoding = genericToEncoding defaultOptions

instance FromJSON Scene where
    parseJSON (Object v) = Scene 0                        <$>
                           v .:? "stepSize"       .!= 0.3 <*>
                           v .:? "bloomStrength"  .!= 0.4 <*>
                           v .:? "bloomDivider"   .!= 25  <*>
                           v .:? "starIntensity"  .!= 0.7 <*>
                           v .:? "starSaturation" .!= 0.7 <*>
                           v .:? "diskColor"
                             .!= PixelHSI 0.16 0.1 0.95   <*>
                           v .:? "diskOpacity"    .!= 0   <*>
                           v .:? "diskInner"      .!= 3   <*>
                           v .:? "diskOuter"      .!= 12  <*>
                           v .:? "resolution"     .!= (1280, 720) <*>
                           v .:? "supersampling"  .!= False

    parseJSON invalid = typeMismatch "Object" invalid

instance ToJSON Scene where
    toEncoding = genericToEncoding defaultOptions


================================================
FILE: src/ImageFilters.hs
================================================
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE Strict #-}

module ImageFilters (bloom, supersample) where

import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as MU
import Data.Massiv.Array
import Data.Massiv.Array.Manifest.Vector
import Data.Massiv.Array.Unsafe
import Data.Massiv.Array.IO
import Control.Monad (replicateM_)
import Control.Applicative (liftA2)
import Graphics.ColorSpace

ix1d :: Int -> Int -> Int -> Int
{-# INLINE ix1d #-}
ix1d !w !y !x = y*w + x

add :: Pixel RGB Double -> Pixel RGB Double -> Pixel RGB Double
add = liftA2 (+)
sub :: Pixel RGB Double -> Pixel RGB Double -> Pixel RGB Double
sub = liftA2 (-)
mul :: Double -> Pixel RGB Double -> Pixel RGB Double
mul a = fmap (a *)

boxBlur :: Int -> Int -> Image U RGB Double -> IO (Image U RGB Double)
boxBlur !r !passes img = let
    myDims@(h :. w) = size img
    rows' = U.enumFromN (0 :: Int) h
    cols' = U.enumFromN (0 :: Int) w

    -- Functions to safely index a vector representing an image with specialized
    -- horizontal / vertical bound checks. Out of bounds indices return a black
    -- pixel.
    {-# INLINE ixh #-}
    {-# INLINE ixv #-}
    {-# INLINE ix1d' #-}
    ix1d' = ix1d w
    ixh v y x
        | x < 0 || x >= w = PixelRGB 0 0 0
        | otherwise = U.unsafeIndex v $ ix1d' y x
    ixv v x y
        | y < 0 || y >= h = PixelRGB 0 0 0
        | otherwise = U.unsafeIndex v $ ix1d' y x

    -- Normalize by the "width" of the kernel
    normFactor :: Double
    {-# INLINE normFactor #-}
    normFactor = 1 / (2*fromIntegral r + 1)

    {-# INLINE blur #-}
    blur writeToVec crds ix1df readf vecIn y = let
        {-# INLINE pix #-}
        -- A function to yield a pixel from the image vector
        pix = readf vecIn y
        -- Starting value
        startVal = U.foldl1' add . U.map pix . U.unsafeTake r $ crds
        {-# INLINE accumulate #-}
        accumulate !rgb x = do
            let newRGB =  (rgb `add` pix (x+r)) `sub` pix (x-r)
            _ <- writeToVec (ix1df y x) $ mul normFactor newRGB
            return newRGB
        -- Sweep over the row / col of the image
        in U.foldM'_ accumulate startVal crds
    in do
        mv <- U.thaw $ toVector img
        let wrt = MU.unsafeWrite mv
        replicateM_ passes $ do
            -- First blur horizontally
            tmp1 <- U.freeze mv
            U.mapM_ (blur wrt cols' ix1d' ixh tmp1) rows'
            -- Then vertically
            tmp2 <- U.freeze mv
            U.mapM_ (blur wrt rows' (flip ix1d') ixv tmp2) cols'
        out <- U.unsafeFreeze mv
        return $ fromVector Par myDims out

bloom :: Double -> Int -> Image U RGB Double -> IO (Image U RGB Double)
bloom strength divider img = do
    let myDims@(_ :. w) = size img
    blurred <- boxBlur (w `div` divider) 3 img
    return . makeArrayR U Par myDims
        $ \ix -> img `unsafeIndex` ix `add`
                 mul strength (blurred `unsafeIndex` ix)

supersample :: Image U RGB Double -> Image U RGB Double
supersample img = let
    h :. w = size img
    {-# INLINE pix #-}
    pix y x = img `unsafeIndex` (y :. x)
    {-# INLINE f #-}
    f (y :. x) = mul 0.25
        $ pix (2*y) (2*x) `add` pix (2*y+1) (2*x) `add` pix (2*y) (2*x+1)
                          `add` pix (2*y+1) (2*x+1)
    in makeArrayR U Par (Sz ((h `div` 2) :. (w `div` 2))) f


================================================
FILE: src/Raytracer.hs
================================================
{-# LANGUAGE StrictData #-}
{-# LANGUAGE Strict #-}

module Raytracer (render, writeImg) where

import Linear hiding (lookAt, mult, trace)
import qualified Linear as L
import Control.Applicative
import Control.Lens
import Data.Default
import Data.Massiv.Array as A
import Data.Massiv.Array.IO
import Graphics.ColorSpace
import Prelude as P

import StarMap
import ConfigFile
import ImageFilters

data Layer = Layer (Pixel RGBA Double) | Bottom (Pixel RGBA Double) | None
data PhotonState = PhotonState (V3 Double) (V3 Double)

sRGB :: Double -> Double
sRGB x = let
  a = 0.055
  in if x < 0.0031308 then 12.92 * x
    else (1 + a) * x ** (1.0 / 2.4) - a

writeImg :: Image U RGB Double -> FilePath -> IO ()
writeImg img path =
    writeArray PNG def path
      . A.map (toWord8 . fmap sRGB) $ img

blend :: Pixel RGBA Double -> Pixel RGBA Double -> Pixel RGBA Double
blend src@(PixelRGBA _ _ _ ta) = let
    comp tc bc = tc + bc * (1 - ta)
    in liftA2 comp src

-- Generate the sight rays ie. initial conditions for the integration
generateRay :: Config -> Ix2 -> PhotonState
generateRay cfg (y' :. x') = PhotonState vel pos
    where cam = camera cfg
          pos = position cam
          scn = scene cfg
          w = fromIntegral . fst $ resolution scn
          h = fromIntegral . snd $ resolution scn
          matr = L.lookAt pos (lookAt cam) (upVec cam) ^. _m33
          vel  = L.normalize . (L.transpose matr !*)
                 $ V3 (fov cam * (fromIntegral x' / w - 0.5))
                      (fov cam * (0.5 - fromIntegral y' / h) * h/w)
                      (-1)

render :: Config -> StarTree -> Image U RGB Double
render cfg startree = let
    scn = scene cfg
    cam = camera cfg
    (w, h) = resolution scn
    res@(w', h') = if supersampling scn then (2*w, 2*h) else (w, h)
    scn' = scn { safeDistance =
                   max (50^(2 :: Int)) (2 * quadrance (position cam))
               , diskInner = diskInner scn ^ (2 :: Int)
               , diskOuter = diskOuter scn ^ (2 :: Int)
               , resolution = res }
    cfg' = cfg { scene = scn' }
    diskRGB = toPixelRGB $ diskColor scn
    img = makeArrayR U Par (h' :. w') $ traceRay cfg' diskRGB startree :: Image U RGB Double
    in if supersampling scn then supersample img else img

traceRay :: Config -> Pixel RGB Double -> StarTree -> Ix2
            -> Pixel RGB Double
traceRay cfg diskRGB startree pt = let
        ray@(PhotonState vel pos) = generateRay cfg pt
        h2 = quadrance $ pos `cross` vel
        scn = scene cfg
    in dropAlpha . colorize scn diskRGB startree h2 $ ray

colorize :: Scene -> Pixel RGB Double -> StarTree -> Double -> PhotonState
            -> Pixel RGBA Double
colorize scn diskRGB startree h2 crd = let
    colorize' rgba crd' = let
        newCrd = rk4 (stepSize scn) h2 crd'
        in case findColor scn diskRGB startree crd' newCrd of
            Layer rgba' -> colorize' (blend rgba rgba') newCrd
            Bottom rgba' -> blend rgba rgba'
            None -> colorize' rgba newCrd
    in colorize' (PixelRGBA 0 0 0 0) crd

findColor :: Scene -> Pixel RGB Double -> StarTree -> PhotonState -> PhotonState
             -> Layer
{-# INLINE findColor #-}
findColor scn diskRGB startree (PhotonState vel pos@(V3 _ y _))
    (PhotonState _ newPos@(V3 _ y' _))
    | r2 < 1 = Bottom (PixelRGBA 0 0 0 1)  -- already passed the event horizon
    | r2 > safeDistance scn = Bottom . addAlpha 1.0
        $ starLookup startree (starIntensity scn) (starSaturation scn) vel
    | diskOpacity scn /= 0 && signum y' /= signum y
        && r2ave > diskInner scn && r2ave < diskOuter scn
        = Layer $ diskColor' scn diskRGB (sqrt r2ave)
    | otherwise = None
    where r2 = quadrance pos
          r2' = quadrance newPos
          r2ave = (y'*r2 - y*r2') / (y' - y)

diskColor' :: Scene -> Pixel RGB Double -> Double -> Pixel RGBA Double
{-# INLINE diskColor' #-}
diskColor' scn diskRGB r = let
    rInner = sqrt (diskInner scn)
    rOuter = sqrt (diskOuter scn)
    intensity = sin (pi * ((rOuter - r) / (rOuter - rInner)) ^ (2 :: Int))
    rgb = fmap (* intensity) diskRGB
    in addAlpha (intensity * diskOpacity scn) rgb

rk4 :: Double -> Double -> PhotonState -> PhotonState
{-# INLINE rk4 #-}
rk4 h h2 y = let
    mul :: Double -> PhotonState -> PhotonState
    {-# INLINE mul #-}
    mul a (PhotonState u v) = PhotonState (u ^* a) (v ^* a)

    add :: PhotonState -> PhotonState -> PhotonState
    {-# INLINE add #-}
    add (PhotonState x z) (PhotonState u v) = PhotonState (x ^+^ u) (z ^+^ v)

    f :: PhotonState -> PhotonState
    {-# INLINE f #-}
    f (PhotonState vel pos) =
        PhotonState (-1.5*h2 / (norm pos ^ (5 :: Int)) *^ pos) vel

    k1 = f y 
    k2 = f $ y `add` mul (h / 2) k1
    k3 = f $ y `add` mul (h / 2) k2
    k4 = f $ y `add` mul h k3
    sumK = k1 `add` mul 2 k2 `add` mul 2 k3 `add` k4
    in y `add` mul (h / 6) sumK


================================================
FILE: src/StarMap.hs
================================================
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE Strict #-}

{-# OPTIONS_GHC -fno-warn-orphans #-}

module StarMap
    ( Star, StarTree, StoredStarTree
    , readMapFromFile, treeToByteString, readTreeFromFile
    , buildStarTree, starLookup ) where

import Control.Monad
import Control.Applicative (liftA2)
import Data.Word
import Data.Char
import Data.Foldable
import qualified Data.ByteString as B
import Data.Serialize as S
import Data.KdMap.Static
import Linear as L
import Graphics.ColorSpace

import Util

type Star = (V3 Double, (Int, Double, Double))
type StarTree = KdMap Double (V3 Double) (Int, Double, Double)
type StoredStar = (V3 Double, (Int, Char))
type StoredStarTree = KdMap Double (V3 Double) (Int, Char)

instance Serialize StoredStarTree
instance Serialize (TreeNode Double (V3 Double) (Int, Char))

-- We can't serialize functions but let's hack around it so that we can
-- serialize the KdMap anyway
instance Serialize (SquaredDistanceFn Double (V3 Double)) where
    put _ = put (0 :: Word8)
    get = skip 1 >> return (defaultSqrDist toList)

instance Serialize (PointAsListFn Double (V3 Double)) where
    put _ = put (0 :: Word8)
    get = skip 1 >> return toList

-- Parse the star list in the binary format specified at
-- http://tdc-www.harvard.edu/software/catalogs/ppm.entry.html
readMap :: Get [StoredStar]
readMap = do
    -- Skip the header
    skip 28
    nBytes <- remaining
    replicateM (nBytes `div` 28) $ do
        ra <- getFloat64be
        dec <- getFloat64be
        spectral <- getWord8
        skip 1
        mag <- getInt16be
        skip 8
        return ( raDecToCartesian ra dec
               , (fromIntegral mag, chr $ fromIntegral spectral) )

starColor' :: (Int, Char) -> (Int, Double, Double)
starColor' (mag, ch) = let (h, s) = starColor ch in (mag, h, s)

-- Some nice colour values for different spectral types
starColor :: Char -> (Double, Double)
starColor 'O' = (0.631, 0.39)
starColor 'B' = (0.628, 0.33)
starColor 'A' = (0.622, 0.21)
starColor 'F' = (0.650, 0.03)
starColor 'G' = (0.089, 0.09)
starColor 'K' = (0.094, 0.29)
starColor 'M' = (0.094, 0.56)
starColor _   = (0, 0)

raDecToCartesian :: Double -> Double -> V3 Double
raDecToCartesian ra dec = V3 (cos dec*cos ra) (cos dec*sin ra) (sin dec)

readMapFromFile :: FilePath -> IO (Either String [StoredStar])
readMapFromFile path = do
    ebs <- readSafe path
    return $ ebs >>= runGet readMap

readTreeFromFile :: FilePath -> IO (Either String StarTree)
readTreeFromFile path = do
    ebs <- readSafe path
    return $ fmap starColor' <$> (S.decode =<< ebs)

treeToByteString :: StoredStarTree -> B.ByteString
treeToByteString = S.encode

buildStarTree :: [StoredStar] -> StoredStarTree
buildStarTree = build toList

starLookup :: StarTree -> Double -> Double -> V3 Double -> Pixel RGB Double
{-# INLINE starLookup #-}
starLookup starmap intensity saturation vel = let
    -- The magnitude value tells about the intensity of the star. The
    -- brighter the star, the smaller the magnitude. These constants are
    -- used for adjusting the dynamics of the rendered celestial sphere.
    max_brightness = 950 -- the "maximum brightness" magnitude
    dynamic = 50         -- "dynamic range": magnitude change that doubles intensity
    w = 0.0005           -- width parameter of the gaussian function

    nvel = L.normalize vel
    stars = inRadius starmap (3 * w) nvel

    renderPixel (pos, (mag, hue, sat)) = let
        d2 = qd pos nvel
        a = log 2 / dynamic
        -- Conversion from the log magnitude scale to linear brightness
        -- and a Gaussian intensity function. This determines the apparent size
        -- and brightness of the star.
        val = (* intensity) . min 1
            . exp $ a * (max_brightness - fromIntegral mag) - d2 / (2 * w^(2 :: Int))
        in toPixelRGB $ PixelHSI hue (saturation * sat) val
    in fmap (min 1) . foldl' (liftA2 (+)) (PixelRGB 0 0 0) $ renderPixel <$> stars


================================================
FILE: src/Util.hs
================================================
module Util ( promptOverwriteFile, readSafe, normalizePath
            , timeAction, padZero ) where

import System.Directory
import System.IO
import qualified Data.ByteString as B
import Data.Time.Clock.POSIX (getPOSIXTime)
import System.FilePath
import Control.DeepSeq

readSafe :: FilePath -> IO (Either String B.ByteString)
readSafe path = do
    exists <- doesFileExist path
    if exists then Right <$> B.readFile path
              else return . Left $ "Error: file " ++ path
                  ++ " doesn't exist.\n"

promptOverwriteFile :: FilePath -> (FilePath -> IO ()) -> IO ()
promptOverwriteFile path doWrite = do
    doesExist <- doesFileExist path
    if doesExist then do
        putStr $ "Overwrite " ++ path ++ "? [y/N] "
        hFlush stdout
        answer <- getLine
        if answer == "y" || answer == "Y" then doWrite path
                                          else putStrLn "Nothing was written."
        else doWrite path

normalizePath :: FilePath -> IO FilePath
normalizePath path = (dropTrailingPathSeparator . normalise)
    <$> makeRelativeToCurrentDirectory path

timeAction :: NFData a => String -> a -> IO a
timeAction actionName value = do
    time1 <- (round <$> getPOSIXTime) :: IO Int
    let res = value
    time2 <- round <$> (res `deepseq` getPOSIXTime)
    let secs = time2 - time1
    putStrLn $ actionName ++ " completed in " ++ show (secs `div` 60)
        ++ " min " ++ show (secs `rem` 60) ++ " sec."
    return res

padZero :: Int -> Int -> String
padZero maxVal val = let
    nDigits x = (floor . logBase 10 $ (fromIntegral x :: Double)) + 1
    nZeros = nDigits maxVal - nDigits val
    zeros = replicate nZeros '0'
    in zeros ++ show val


================================================
FILE: stack.yaml
================================================
resolver: lts-13.16
flags: {}
extra-package-dbs: []
packages:
- '.'
Download .txt
gitextract_5lpryipi/

├── .gitignore
├── LICENSE
├── README.md
├── Setup.hs
├── animations/
│   └── default-ani.yaml
├── app/
│   ├── Animate.hs
│   ├── GenerateTree.hs
│   └── Main.hs
├── blackstar.cabal
├── scenes/
│   ├── closeup.yaml
│   ├── default-aa.yaml
│   ├── default.yaml
│   ├── fartheraway.yaml
│   ├── lensing-disk.yaml
│   ├── lensing.yaml
│   ├── wideangle-disk.yaml
│   ├── wideangle.yaml
│   └── wideangle1.yaml
├── scripts/
│   └── ffmpeg-animate
├── src/
│   ├── Animation.hs
│   ├── ConfigFile.hs
│   ├── ImageFilters.hs
│   ├── Raytracer.hs
│   ├── StarMap.hs
│   └── Util.hs
└── stack.yaml
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (47K chars).
[
  {
    "path": ".gitignore",
    "chars": 143,
    "preview": ".stack-work\nblackstar.prof\nout.png\ntexture.jpg\nBSC5\nSAO.pc\nPPM\nPPMra\nrenders\nbloomed.png\noutput\n*.png\n!example.png\nframe"
  },
  {
    "path": "LICENSE",
    "chars": 1527,
    "preview": "Copyright Sakari Kapanen (c) 2016\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or with"
  },
  {
    "path": "README.md",
    "chars": 6398,
    "preview": "# Blackstar\nA black hole ray tracer written in Haskell. There's [an article](https://flannelhead.github.io/projects/blac"
  },
  {
    "path": "Setup.hs",
    "chars": 46,
    "preview": "import Distribution.Simple\nmain = defaultMain\n"
  },
  {
    "path": "animations/default-ani.yaml",
    "chars": 1147,
    "preview": "# Animation files are quite similar to scene files. The difference is that you\n# can define multiple camera instances wh"
  },
  {
    "path": "app/Animate.hs",
    "chars": 2482,
    "preview": "{-# LANGUAGE DeriveDataTypeable #-}\n{-# OPTIONS_GHC -fno-cse #-}\n\nmodule Main where\n\nimport Prelude hiding (writeFile)\ni"
  },
  {
    "path": "app/GenerateTree.hs",
    "chars": 1010,
    "preview": "module Main where\n\nimport qualified Data.ByteString.Lazy as B\nimport System.Environment (getArgs)\n\nimport Util\nimport St"
  },
  {
    "path": "app/Main.hs",
    "chars": 4670,
    "preview": "{-# LANGUAGE DeriveDataTypeable #-}\n{-# OPTIONS_GHC -fno-cse #-}\n\nmodule Main where\n\nimport System.Directory\nimport Cont"
  },
  {
    "path": "blackstar.cabal",
    "chars": 2526,
    "preview": "name:                blackstar\nversion:             0.1.0.0\nsynopsis:            A black hole ray tracer\ndescription:   "
  },
  {
    "path": "scenes/closeup.yaml",
    "chars": 270,
    "preview": "camera:\n    position:   [10, 1, -2]\n    lookAt:     [0, 0, 6]\n    upVec:      [0, 1, 0]\n    fov:        1.2\n\nscene:\n    "
  },
  {
    "path": "scenes/default-aa.yaml",
    "chars": 334,
    "preview": "camera:\n    position:   [0, 1, -20]\n    lookAt:     [2, 0, 0]\n    upVec:      [-0.2, 1, 0]\n    fov:        1.5\n\nscene:\n "
  },
  {
    "path": "scenes/default.yaml",
    "chars": 1446,
    "preview": "camera:\n    # All the vectors are [x, y, z] coordinates\n    position:   [0, 1, -20]  # The position of the camera\n    lo"
  },
  {
    "path": "scenes/fartheraway.yaml",
    "chars": 303,
    "preview": "camera:\n    position:   [-25, 1, -60]\n    lookAt:     [-12, -4, 0]\n    upVec:      [0.15, 1, 0]\n    fov:        2\n\nscene"
  },
  {
    "path": "scenes/lensing-disk.yaml",
    "chars": 297,
    "preview": "camera:\n    position:   [30, 0.4, 3]\n    lookAt:     [0, 0, 0]\n    upVec:      [0, 1, 0.2]\n    fov:        1\n\nscene:\n   "
  },
  {
    "path": "scenes/lensing.yaml",
    "chars": 295,
    "preview": "camera:\n    position:   [30, 0.4, 3]\n    lookAt:     [0, 0, 0]\n    upVec:      [0, 1, 0.2]\n    fov:        1\n\nscene:\n   "
  },
  {
    "path": "scenes/wideangle-disk.yaml",
    "chars": 305,
    "preview": "camera:\n    position:   [-6, 1, -20]\n    lookAt:     [-6, -4, 0]\n    upVec:      [-0.2, 1, 0]\n    fov:        3.5\n\nscene"
  },
  {
    "path": "scenes/wideangle.yaml",
    "chars": 258,
    "preview": "camera:\n    position:   [20, 0, 0]\n    lookAt:     [0, 0, 3.5]\n    upVec:      [0, 1, 0]\n    fov:        2\n\nscene:\n    r"
  },
  {
    "path": "scenes/wideangle1.yaml",
    "chars": 258,
    "preview": "camera:\n    position:   [0, 0, 20]\n    lookAt:     [3.5, 0, 0]\n    upVec:      [0, 1, 0]\n    fov:        2\n\nscene:\n    r"
  },
  {
    "path": "scripts/ffmpeg-animate",
    "chars": 271,
    "preview": "#!/bin/sh\n\n# Create YouTube quality video from stills using ffmpeg\n#\n# USAGE:\n# ffmpeg-animate PREFIX\n#\n# Writes to file"
  },
  {
    "path": "src/Animation.hs",
    "chars": 3289,
    "preview": "{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, DeriveGeneric #-}\n\nmodule Animation ( Keyframe(camera, time)\n      "
  },
  {
    "path": "src/ConfigFile.hs",
    "chars": 3041,
    "preview": "{-# LANGUAGE OverloadedStrings, FlexibleInstances, DeriveGeneric #-}\n{-# OPTIONS_GHC -fno-warn-orphans #-}\n\nmodule Confi"
  },
  {
    "path": "src/ImageFilters.hs",
    "chars": 3365,
    "preview": "{-# LANGUAGE BangPatterns #-}\n{-# LANGUAGE StrictData #-}\n{-# LANGUAGE Strict #-}\n\nmodule ImageFilters (bloom, supersamp"
  },
  {
    "path": "src/Raytracer.hs",
    "chars": 4882,
    "preview": "{-# LANGUAGE StrictData #-}\n{-# LANGUAGE Strict #-}\n\nmodule Raytracer (render, writeImg) where\n\nimport Linear hiding (lo"
  },
  {
    "path": "src/StarMap.hs",
    "chars": 4001,
    "preview": "{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}\n{-# LANGUAGE StrictData #-}\n{-# LANGUAGE Strict #-}\n\n{-# OPTION"
  },
  {
    "path": "src/Util.hs",
    "chars": 1696,
    "preview": "module Util ( promptOverwriteFile, readSafe, normalizePath\n            , timeAction, padZero ) where\n\nimport System.Dire"
  },
  {
    "path": "stack.yaml",
    "chars": 68,
    "preview": "resolver: lts-13.16\nflags: {}\nextra-package-dbs: []\npackages:\n- '.'\n"
  }
]

About this extraction

This page contains the full source code of the flannelhead/blackstar GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (43.3 KB), approximately 12.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!