OpenArena Message Boards

OpenArena Contributions => Graphics => Topic started by: cheb on April 19, 2018, 03:13:20 AM

Title: My new sky initiative :p
Post by: cheb on April 19, 2018, 03:13:20 AM
Hi, guys  xD
I was working on a mod for [some proprietary game] and it made me realize how human-friendly spherical projection is when working with skyboxes. Unless the angle is too up or down (in the range of +/-30 degrees it's fine I'd say) moving hills and mountains in The GIMP up and down, left and right is very easy and convenient.

So. Keeping in mind the appaling lack of GPL tools to work with cube maps, and corresponding dearth of OA skies, I began working on what I call a skybox composer. I wanted to present a finished product here, but it is slow going - involves tons ov trigonometry which I am a total noob at. Enough to say, I liked math back in school but my grade was always around 3..3+ (C). I have very goood intuitive understanding, but formulae... Argh.

It would be a small but powerful thing, taking a text definition file and a bunch of images and composing them into a sphere or cube map. A bit like command-line Photoshop, if you like. There would be layers, layer groups with blend modes, layer masks and all that jazz. All in about two thousand lines of source code.

The application (besides making skies for [that other game]) would allow for easily maintained and modified skyboxes. For instance, you'd be able to put planets, nebulae or other sprites at arbitrary angles, scales and rotations - and they would be projected correctly. And it keeps the separation of sources (images) and compiled result (the output cubemap) for ease of future modifications!

The license is LGPL as dictated by the image library I use (which is licensed under MPL 1.1 / alternatively LGPL). The application would have a GUI version (for easier re-run with one click) and, maybe, built-in OpenGL viewer of results (later), as well as a command-line version for both Windows and Linux (and, possibly, arm/Raspberry Pi 3). Cool features would include selective Gaussian blur in an up to 270-degree cone (leave a last will to your great-grandkids to see it completing its run).

I hope I'd be able to present an unfinished barebones prototype in a few days. The principles are really too easy. But the math! Argh.  Vector this, matrix that, multiply them how... >:(


Title: Re: My new sky initiative :p
Post by: fromhell on April 19, 2018, 07:11:45 PM
My current skybox-making technique is this in Blender:

- make a cube and uvmap that cube to the 6 skybox textures
- make a duplicate of that cube
- subdivide the cube
- spherify the cube in a shape key
- make another UV layer on your "cube" (read: it looks like a sphere at this point). Uvmap out some hemispheres on a new single texture
- paint on new single texture
- refresh your skybox images by 0'ing the shape key and rendering the sphere cube to the cube you made before

Title: Re: My new sky initiative :p
Post by: cheb on April 20, 2018, 02:04:54 AM
Aha! So it *is* possible!  :)

But... B̵̖͍̻̗͚̺l̴̼̲̦͍͙̖e̴̢̮̻͎̺͜n̵͈̮͎͖͔͜d̷̘̥͓̘͖͕e̷̡̤̫͔̻̗ŗ̶̝͚͔͇̺! [shudders] :( The interface made by Chthulhu in Hell! I was as afraid touching it 10 years ago as I am now. I only managed to make a single sphere-mapped sphere md3 by following a step-by-step tutorial on YouTube, and only because time was essential. Because learning the md3 format specification, then coding a sphere generator that generated the required md3 field-by-field would be more intuitive and less taxing on my sanity.  :no:

I stand by my project because anything that lets people do things while avoiding Blender is a must. It's not about choice, it's about saving the innocent minds, bro!

Title: Re: My new sky initiative :p
Post by: Gig on April 20, 2018, 04:19:04 AM
it's about saving the innocent minds, bro!
If you are talking to Fromhell, I think you should say "sis". ^_^

Title: Re: My new sky initiative :p
Post by: cheb on April 20, 2018, 11:59:53 AM
Yea. :-[

Anyways, almost made it work, copied a picture 1:1 (with R and B swapped - why, oh God, why) and then began crashing on each run with weird error messages. Im worn out, would continue tomorrow. xD

P.S. A case of delayed shooting myself in the foot.
ImagingJpeg.pas had following conditionals:
{$IF Defined(FPC) and Defined(PASJPEG)}
  { When using FPC's pasjpeg in FPC the channel order is BGR instead of RGB}
where {$DEFINE RGBSWAPPED} was commented out  :no:
So *of course* it was loading jpegs - and only jpegs - with r and b swapped  >:(
I have a sinking suspicion I know the idiot who commented that out. I see him every day in the mirror.  :-X

Title: Re: My new sky initiative :p
Post by: cheb on April 21, 2018, 12:26:05 PM
..almost... there... [falls down exhausted]  x(

Basic functionality *does* work now, only works with sphere maps, supporting all layer blend modes. See tomorrow: actual useful functionality, like output to cubemaps, layer types for actual planting sprites of arbitrary size and orientation into the sphere, layer types for functions like curves, blur/unsharp mask, constant color layers, et cetera.

Here: combining two arbitrary photos treating them like they were sphere maps.



Title: Re: My new sky initiative :p
Post by: cheb on April 22, 2018, 10:10:00 AM
Groan. I spent so much time debugging. It drank so much of my blood  :no:

All because I mistyped *twice* when implementing this rotation matrix in my code:

And it *almost* worked. Most of the time. Until I applied the roll angle.

I'm spent.  x(


Title: Re: My new sky initiative :p
Post by: Gig on April 22, 2018, 12:51:37 PM
Looking forward for seeing a map of yours!  :)

Title: Re: My new sky initiative :p
Post by: cheb on April 24, 2018, 09:23:01 AM
Oh, I will!

Probably next week, as making skies for [that other game] is urgent.
For now, implemented: q3 compatible cube map output (but not reading!), layer groups, solid color layers  B,

For the future... I think, why not *distortion* layer blending modes, like lens effects? Because it all works in 3d vector space anyway.
Also, I haven't implemented layer masks yet.

Note the bottommost sprite, wrapped more than halfway around the sky. The max angle for a square sprite is 254 degrees!  :giggity:

P.S. Also forgot: the option to mirror the sprite or cubemap; the option to force non-native aspect ratio on a sprite

Title: Re: My new sky initiative :p
Post by: cheb on May 13, 2018, 04:09:59 AM
Nothing useful for OA - yet - but I finally presernt you the tool:

Full source code (LGPL) and usage instruction (readme.txt) inside.
I will start making skies for OA at the same time as I create a dedicated website for this program.

This week I overcame a significant hurdle: GZDoom sky export.
This format is grossly inefficient, stretched non-linearly in a tricky pattern: while the texture has to be 1024 x 512 to avoid auto-repeating, the horizon line is only at 207 pixels from its top.
Trial and error, guys, trial and error. Sniff.

The warped parody of a sphere map GZDoom requires:

The sphere map it was made from (see how smoth and even it is!):
(warning: not a free image, generated by proprietary software tool!)

Sanity check:
( (

 procedure TOutDoomSkyLayer.BitmapToProjectVector(const x, y: float; out vector: TVector3f);
    pitch, ynl, nlf, yaw, ax, az: float;
    yaw:= (0 - (x / (image.width - 1))) * 2 * Pi;
    // this shit is *grossly* non-linear!!!
    // top part of sky is half of 415 pixels, with 20 degrees in zenith lost,
    //  the game engine fills it with blurred color derived from the top image edge.
    if y < GZDoomSkyMiddle  then begin
        ynl:= (GZDoomSkyMiddle - y) * (GZDoomSkyTopPart / (GZDoomSkyMiddle * 2));
        //it is stretched non-linearly! The middle is larger.
        nlf:= 1 + (0.5 - ynl / GZDoomSkyTopPart)  * 0.5 * 2;
        ynl*= nlf;
    // nadir. The same story here, blurred color derived from the bottom image edge
    else begin
      ynl := - (y - GZDoomSkyMiddle) * (GZDoomSkyBottomPart / ((GZDoomSkyHeight - 1 - GZDoomSkyMiddle) * 2));
      nlf:= 1 + (0.5 + ynl / GZDoomSkyBottomPart)  * 0.78 * 2;
      ynl*= nlf;
    pitch:= ynl * Pi;
    vector[0]:= cos(pitch) * sin(yaw);
    vector[1]:= sin(pitch);
    vector[2]:= cos(pitch) * - cos(yaw);

For comparison, the same code for sane output formats:
 procedure TOutSphereMapLayer.BitmapToProjectVector(const x, y: float; out vector: TVector3f);
  var pitch, yaw, ax, az: float;
    //vector is in opengl coordinates (x right, y up, z pokes at your eye)
    //bitmap coordinates are 0,0 = top left corner
    if NonClosedHorizontally
      then yaw:= (-0.5 + (x / image.width)) * 2 * Pi
      else yaw:= (-0.5 + (x / (image.width - 1))) * 2 * Pi;
    pitch:= (0.5 - (y / (image.height - 1))) * Pi;
    vector[0]:= cos(pitch) * sin(yaw);
    vector[1]:= sin(pitch);
    vector[2]:= cos(pitch) * - cos(yaw);

  procedure TOutCubeMapLayer.BitmapToProjectVector(const x, y: float; out vector: TVector3f);
  //correct rotation to one of the 6 sides is provided by the matrix
    FillChar(vector, sizeof(vector), 0);
    vector[0]:= sin((Pi / 2) * (0.5 - (x / (image.width - 1))));
    vector[1]:= sin((Pi / 2) * (0.5 - (y / (image.height - 1))));
    vector[2]:= - 1 / sqrt(2);
    vector*= matrix;

Oh, and have this nice sphere-mapped md3:
Be thankful, I made it in Blender. In Blender, man!  :o Permanently losing some SANity points in the process  @_@ because Blender interface was made by Chthulhu in Hell.

CheSkymp is a skybox composer optimized for fast reassembly with one click/launch. Meaning it operates in the same way as compilers do: There are sources, in this case images and the sky definition .INI file that are processed into output skyboxes.

The main advantage of this is the ability to keep several output skyboxes synchronized without doing that manually (e.g. you have one hard-alpha sky map and one blurred shadow sphere map).

A secondary advantage is that your sources are not changed during the composition process so there are no accumulating errors.

The main disadvantage is non-visual, you have to keep your skybox layout in your mind.

To work, CheSkymp needs a sky definition (using .INI file format) that describes the skybox(es) to make

NOTE#1: the [section]:ident notation I use in this readme refers to a string
inside a block delineated by an opening string
That is all there is about the .INI file "syntax".

NOTE#2: all color calculations are performed in floating point and only culled to 0.0..1.0 when written to the output image. So layer blending modes like myltiply *can* surprise you, you *can* specify out-of-range colors like 2.0,2.2,11 and you *can* get and use *negative* brightness values.

NOTE#3: the decimal separator in floating-point values is dot, regardless of system locale and stuff.

NOTE#4: absolutely all operations on the input bitmaps use Lanczos filtering.

NOTE#5: the difinition is NOT case-sensitive. Unless you work with file names in Linux, there is absolutely no distinction between mylayer and MyLaYeR

   Input path where CheSkymp searches for source images. If not specified, the path to the .INI file will be used. Can be declared as relative.
   Output path where CheSkymp places the generated skybox bitmaps. If not specified, the .INI file path will be used.  Can be declared as relative.

   If set to 1, will screen any exceptions during bitmap generation, filling these pixels with magenta.
   Default is tga. Please note this is file extension without the dot that is passed directly to Vampyre Imaging Library. No checks are performed. It is up to you to use right kinds of formats and not try saving image with transparency into a JPEG.
   A list of comma-separated output layer identifiers. Please note that output image name is (usually) output layer identifier with extension added.
   Note that outputs can share input layers!
[<output layer>]:type
   The type of skybox being generated. Valid values are:
      Bog standard sphere map. Forward vector (zero direction) is the exact middle of the bitmap.
      A specific kind of sphere map, distorted non-linearly to counteract non-linear stretching in GZDoom). Zenith and nadir regions are not present (so there is information loss when exporting tinto this format). The game engine fills zenith and nadir with solid color derived from the image edges averaged.
      The bitmap size is forced 1024x512
      A cube map in the format used by Quake 3 and games built on its engine, like Open Arena. Six images will be generated, with suffixes _ft, _lf, _rt, _bk, _up and _dn.

[<output layer>]:layers
   A comma-separated list of layer identifiers
   Nothing is stopping you from including the same layer several times.
[<output layer>]:input_path
   Optional. Overrides the project-wide setting.

[<output layer>]:output_path
   Optional. Overrides the project-wide setting.

[<output layer>]:output_image_format
   Optional. Overrides the project-wide setting.

[<output layer>]:supersampling
   Optional. Overr-- NOT IMPLEMENTED YET, DAMMIT :(
[<output layer>]:alpha
   Boolean value (set to 1 to activate). Determines if the image would have alpha channel. If not, black background would be in stead of transparent areas. Of course, output image format has to support it.
[<output layer>]:hdr
   Boolean value (set to 1 to activate). Output image would be unculled floating-point RGBA32F. Of course output image format has to support this.
[<output layer>]:flip
   Boolean value (set to 1 to activate). Causes the output image to be flipped horizontally.
   NOT supported by cube maps.

[<output sphere map layer>]:height   
[<output sphere map layer>]:sphere_width
   You can specify only one of them, then another one would be derived from assumption that width is double the height.
   Width is clipped to 8192, height to 4096
[<output sphere map layer>]:non_closed_horizontally   
   Boolean value (set to 1 to activate). Depending on the method you use to render your sphere map, it would be proper (with interpolation between the right and left side texels where the edges meet). By default it is assumed that you use a crude hack of a spherical model with its edges welded shut, so the right and the left map sides are exactly the same position and their pixels must be equal. basically, you sacrifice one pixel of your equator length for the sake of simplicity.
[<output cube map layer>]:height
   Width is always equal to height.
[<input layer>]:type
   The type of layer. Note that most layers support variety of blending modes a la Photoshop!
      Layer group. Has its own blending mode as well as the list of child layers. see below.
      Sphere map.
      A single image projected onto the skybox.
      Solid color. See below.
      A conical gradient. See below.
[<input layer>]:bitmap
   Specifies image file for sphere map and sprite type layers, ignored otherwise.

[<input layer>]:flip
   Boolean value (set to 1 to activate). Causes the input image to be flipped horizontally.
[<input layer>]:opacity
   Floating-point value that defines the layer opacity. Default is 1.0
[<input layer>]:mode
   Blending mode. Default is Normal. Valid values are:
   For what they do, refer to any Photoshop / TheGIMP tutorial.
   Note that hue, saturation and color work differently from classic Photoshop behavior. When encountering source pixels with absolutely no color, white "color" will be produced instead of skipping that pixel and making the layer transparent.

[<input layer>]:yaw   
[<input layer>]:pitch   
[<input layer>]:roll
   Floating point numbers specifying layer rotations in degrees.
   Initially, any input layer is positioned around the forward vector (dead center of the sphere map bitmap).
   First, yaw (rotation around the vertical axis) is applied. Positive angle is right.
   Second, pitch (rotation around horizontal axis perpendiculat the direction vector) is applied. Positive angle is up (90=zenith, -90=nadir)
   Finally, roll (rotation around the direction vector) is applied. Positive angle is clockwise.
   Have no effect on solid color layers.
[<input layer>]:y_shift   
   Alternate way to specify pitch for sphere map and sprite type layers (ignored otherwise).
   The angle is in input bitmap pixels.
   Positive shift is UP.

[<input layer>]:x_shift   
   Alternate way to specify yaw for sphere map and sprite type layers (ignored otherwise).
   The angle is in input bitmap pixels.
   Positive shift is right.
NOTE: the sprite's direction vector goes through the center of its bitmap, so sprite with zero yaw and pitch will land in the center of the output sphere map.
[<sprite layer>]:angle
   Floating-point value in degrees dictating the size of the sprite on the sky. The angle is taken across the mid-point, across width or height (whichever is greater). Sprite edges cannot touch together, so maximum attainable angle for a square image is about 254 degrees (may be significantly larger for thin strips, but always less than 360).
[<sprite layer>]:size   
   Alternative way to set angle.
   This value is in pixels of the output bitmap. The resulting angle is calculated relative to the sphere map width (a good enough approximation), or double the cube map height (which is rough and imprecise).
[<sprite layer>]:force_aspect
   Floating-point value. Forces aspect (height to width ratio) regardless of the bitmap's actual dimensions.
[<sprite layer>]:aspect_correction   
   Floating-point value. Aspect (height to width ratio) is multiplied by this.
   Ignored if force_aspect is set.
[<solid color layer>]:color
   Three or four floating-point values separated by commas, for red, green, blue and optional alpha.
   If alpha is omitted, it is set to 1.0
[<gradient layer>]:start_color
   Three or four floating-point values separated by commas, for red, green, blue and optional alpha.
   If alpha is omitted, it is set to 1.0
[<gradient layer>]:end_color
   OPTIONAL. If omitted, the end color is the same as the start color but with alpha set to 0.
   Three or four floating-point values separated by commas, for red, green, blue and optional alpha.
   If alpha is omitted, it is set to 1.0
[<gradient layer>]:start_angle
[<gradient layer>]:end_angle
   Floating-point values culled to 0.0..180.0
   Please note that *all* gradients are conical, due to spherical nature of the sky space.
   If any omitted, 0 is assumed but you *have* to declare either.
   Angles less than start angle are filled with start color, and angles greater than the end angle are filled with end color.
      A fuzzy spot: end_angle=30, no start_angle
      A clear spot in a sphere of solid color: start_angle=30, no end_angle
      A basic sky with a blurred horizon: start_angle=80 end_angle=100, pitch=90, start color blue and end color brown.
[<gradient layer>]:power
   Floating-point value that dictates how non-linear the gradient would be.
   Culled to 0.0001..10000.0
   Default is 1.0
   Basically, a power of transitional value between 0.0 (start) and 1.0 (end) would be taken. Remember how power functions behave in this range: 0.5 is square root, so mid-point would be shifted towards the start. 2 is square, so mid-point would be shifted towards the end.
[<group layer>]:layers
   A comma-separated list of layer identifiers
   Please note that groups can share layers without limit! Make sure you don't create circular references or CheSkymp would hang.

Title: Re: My new sky initiative :p
Post by: cheb on November 20, 2019, 11:46:40 AM
Update: updated the app to be compatible with Free Pascal 3 (download link stays the same), performed dramatic bug hunt and optimization wrapping all numeric constants in code into type-casts to float (did not help any, still slow as hell) and finally made something useful.

This has no input bitmaps, made of conical gradient layers only. Sky intended for ctf.

( ( ( ( ( (