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.
procedure TOutDoomSkyLayer.BitmapToProjectVector(const x, y: float; out vector: TVector3f);
var
pitch, ynl, nlf, yaw, ax, az: float;
begin
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;
end
// 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;
end;
pitch:= ynl * Pi;
vector[0]:= cos(pitch) * sin(yaw);
vector[1]:= sin(pitch);
vector[2]:= cos(pitch) * - cos(yaw);
end;
procedure TOutSphereMapLayer.BitmapToProjectVector(const x, y: float; out vector: TVector3f);
var pitch, yaw, ax, az: float;
begin
//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);
end;
procedure TOutCubeMapLayer.BitmapToProjectVector(const x, y: float; out vector: TVector3f);
//correct rotation to one of the 6 sides is provided by the matrix
begin
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);
Normalize(vector);
vector*= matrix;
end;
Be thankful, I made it in Blender. In Blender, man!
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
ident=<value>
inside a block delineated by an opening string
[section]
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
[project]:input_path
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.
[project]:output_path
Output path where CheSkymp places the generated skybox bitmaps. If not specified, the .INI file path will be used. Can be declared as relative.
[project]:debug
If set to 1, will screen any exceptions during bitmap generation, filling these pixels with magenta.
[project]:output_image_format
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.
[project]:supersampling
NOT IMPLEMENTED YET
[project]:output
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:
sphere_map
Bog standard sphere map. Forward vector (zero direction) is the exact middle of the bitmap.
gzdoom_sky
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
q3_cube_map
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
!UNTESTED!
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!
group
Layer group. Has its own blending mode as well as the list of child layers. see below.
sphere_map
Sphere map.
sprite
A single image projected onto the skybox.
color
Solid color. See below.
gradient
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:
Normal
Multiply
Divide
Screen
Overlay
Dodge
Burn
Hard_Light
Soft_Light
Grain_Extract
Grain_Merge
Difference
Addition
Substract
Darken_Only
Lighten_Only,
Hue
Saturation
Color
Value
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.
Examples:
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.