Skip to content

Instantly share code, notes, and snippets.

@PrimaryFeather
Last active January 15, 2021 08:52
A Starling DisplayObject that displays animated god-rays / sunbeams within a specific area.
package starling.extensions
{
import flash.display.BitmapData;
import starling.animation.IAnimatable;
import starling.display.Quad;
import starling.textures.Texture;
import starling.utils.MathUtil;
/** A quad that efficiently renders a 2D light ray effect on its surface.
*
* <p>This class is useful for adding atmospheric effects, like the typical effects you see
* underwater or in a forest. Add it to a juggler or call 'advanceTime' so that the effect
* becomes animated.</p>
*
* <p>Play around with the different settings to make it suit the style you want. In addition
* to the class-specific properties, you can also assign an overall color or different colors
* per vertex.</p>
*/
public class GodRayPlane extends Quad implements IAnimatable
{
private static const TEXTURE_HEIGHT:int = 32;
private static const TEXTURE_WIDTH:int = 512;
private var _bitmapData:BitmapData;
private var _speed:Number;
private var _size:Number;
private var _skew:Number;
private var _fade:Number;
/** Create a new instance with the given size. Using a "packed" texture format produces
* a slightly different effect with visible gradient steps. */
public function GodRayPlane(width:Number, height:Number, textureFormat:String="bgra")
{
super(width, height);
_speed = 0.1;
_size = 0.1;
_skew = 0.0;
_fade = 1.0;
_bitmapData = new BitmapData(TEXTURE_WIDTH, TEXTURE_HEIGHT, false);
texture = Texture.empty(TEXTURE_WIDTH, TEXTURE_HEIGHT, true, false, false,
1.0, textureFormat, true);
updateTexture();
textureRepeat = true;
style = new GodRayStyle();
}
/** Disposes the internally used texture. */
override public function dispose():void
{
super.dispose();
_bitmapData.dispose();
texture.dispose();
}
private function updateTexture():void
{
_bitmapData.perlinNoise(TEXTURE_WIDTH * _size, TEXTURE_HEIGHT * 0.2,
2, 0, true, true, 0, true);
texture.root.uploadBitmapData(_bitmapData);
}
private function updateVertices():void
{
vertexData.setPoint(2, "texCoords", -_skew, 1.0);
vertexData.setPoint(3, "texCoords", -_skew + 1.0, 1.0);
vertexData.setAlpha(2, "color", 1.0 - _fade);
vertexData.setAlpha(3, "color", 1.0 - _fade);
setRequiresRedraw();
}
/** @inheritDoc */
public function advanceTime(time:Number):void
{
if (_speed > 0 && time > 0)
godRayStyle.offsetY += time * _speed;
}
private function get godRayStyle():GodRayStyle { return style as GodRayStyle; }
/** The speed with which the effect is animated. A value of '1.0' causes the pattern
* to repeat exactly after one second. Range: 0 - infinite. @default 0.1 */
public function get speed():Number { return _speed; }
public function set speed(value:Number):void
{
_speed = MathUtil.max(0, value);
}
/** Determines up the angle of the light rays.
* Range: -5 - 5. @default: 0.0 */
public function get skew():Number { return _skew; }
public function set skew(value:Number):void
{
_skew = MathUtil.clamp(value, -5, 5);
updateVertices();
}
/** Determines the change in the light ray's angles over the width of the plane.
* Range: -1 - 10. @default: 0.0 */
public function get shear():Number { return godRayStyle.shear; }
public function set shear(value:Number):void { godRayStyle.shear = value; }
/** The width of the rays. As a rule of thumb, one divided by this value will yield the
* approximate number of rays. Range: 0.0001 - 1. @default: 0.1 */
public function get size():Number { return _size; }
public function set size(value:Number):void
{
_size = MathUtil.clamp(value, 0.0001, 1);
updateTexture();
}
/** Indicates how the light rays fade out towards the bottom. Zero means no fading,
* one means that the rays will become completely invisible at the bottom.
* Range: 0 - 1, @default: 1 */
public function get fade():Number { return _fade; }
public function set fade(value:Number):void
{
_fade = MathUtil.clamp(value, 0, 1);
updateVertices();
}
/** The distinctiveness and brightness of the light rays.
* Range: 0 - infinite, @default: 1 */
public function get contrast():Number { return godRayStyle.contrast; }
public function set contrast(value:Number):void { godRayStyle.contrast = value; }
/** The current offset used when sampling the noise-texture.
* This is the value that is animated via the 'speed' property.
* If you want to animate the value yourself, set 'speed' to zero.
* Range: 0 - 1, @default: 0 */
public function get textureOffset():Number { return godRayStyle.offsetY; }
public function set textureOffset(value:Number):void{ godRayStyle.offsetY = value; }
}
}
import flash.display3D.Context3D;
import flash.display3D.Context3DProgramType;
import starling.display.Mesh;
import starling.rendering.MeshEffect;
import starling.rendering.Program;
import starling.rendering.VertexDataFormat;
import starling.styles.MeshStyle;
import starling.utils.MathUtil;
class GodRayStyle extends MeshStyle
{
public static const VERTEX_FORMAT:VertexDataFormat =
MeshStyle.VERTEX_FORMAT.extend("settings:float3");
private var _offsetY:Number;
private var _shear:Number;
private var _contrast:Number;
public function GodRayStyle()
{
_offsetY = 0.0;
_shear = 0.0;
_contrast = 1.0;
}
override public function copyFrom(meshStyle:MeshStyle):void
{
var godRayStyle:GodRayStyle = meshStyle as GodRayStyle;
if (godRayStyle)
{
_offsetY = godRayStyle._offsetY;
_shear = godRayStyle._shear;
_contrast = godRayStyle._contrast;
}
super.copyFrom(meshStyle);
}
override public function createEffect():MeshEffect
{
return new GodRayEffect();
}
override public function get vertexFormat():VertexDataFormat
{
return VERTEX_FORMAT;
}
override protected function onTargetAssigned(target:Mesh):void
{
updateVertices();
}
private function updateVertices():void
{
if (target)
{
vertexData.setPremultipliedAlpha(false, true);
var numVertices:int = vertexData.numVertices;
for (var i:int=0; i<numVertices; ++i)
vertexData.setPoint3D(i, "settings", _offsetY, _shear, _contrast);
setRequiresRedraw();
}
}
public function get shear():Number { return _shear; }
public function set shear(value:Number):void
{
_shear = MathUtil.clamp(value, -1, 10);
updateVertices();
}
public function get offsetY():Number { return _offsetY; }
public function set offsetY(value:Number):void
{
while (value > 1.0) value -= 1.0;
_offsetY = value;
updateVertices();
}
public function get contrast():Number { return _contrast; }
public function set contrast(value:Number):void
{
_contrast = MathUtil.max(0, value);
updateVertices();
}
}
class GodRayEffect extends MeshEffect
{
private static const sConstants:Vector.<Number> = new <Number>[0, 1, 2, 0.5];
public function GodRayEffect()
{ }
override protected function createProgram():Program
{
var vertexShader:String = [
"m44 op, va0, vc0", // 4x4 matrix transform to output clip-space
"mov v0, va1 ", // pass texture coordinates to fragment program
"mov v1.xyz, va2.xyz", // copy color to v1.xyz
"mul v1.w, va2.w, vc4.w", // copy combined alpha to v1.w
"mov v2, va3 " // pass settings to fp
].join("\n");
var fragmentShader:String = [
// offset
"mov ft0, v0",
"mov ft0.y, v2.x", // texture coordinates: v = offset
// shear
"mul ft2.x, v0.y, v2.y", // shear *= v
"add ft2.x, ft2.x, fc5.y", // shear = 1 + v * shear
"div ft0.x, ft0.x, ft2.x", // texture coordinates: divide 'u' by shear
// texture lookup
tex("ft1", "ft0", 0, texture),
// contrast
"mul ft1.xyz, ft1.xyz, v2.zzz", // tex color *= contrast
"sub ft2.xyz, fc5.yyy, v2.zzz", // ft2 = 1 - contrast
"add ft1.xyz, ft1.xyz, ft2.xyz", // tex color += ft2
// alpha + tinting
"mul ft1.w, ft1.x, v1.w", // multiply with vertex alpha
"mul ft1.xyz, ft1.xxx, v1.xyz", // tint with vertex color
"mul ft1.xyz, ft1.xyz, ft1.www", // premultiply alpha
// copy to output
"mov oc, ft1"
].join("\n");
return Program.fromSource(vertexShader, fragmentShader);
}
override public function get vertexFormat():VertexDataFormat
{
return GodRayStyle.VERTEX_FORMAT;
}
override protected function beforeDraw(context:Context3D):void
{
super.beforeDraw(context);
context.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 5, sConstants);
vertexFormat.setVertexBufferAt(3, vertexBuffer, "settings");
}
override protected function afterDraw(context:Context3D):void
{
context.setVertexBufferAt(3, null);
super.afterDraw(context);
}
}
@tadeubas
Copy link

Hey Daniel, I am seeing the effect on the screen but it appear to darken everything, like there is a issue with the alpha:

god_rays_alpha_not_100

var godRays:GodRayPlane = new GodRayPlane(worldWidth/4, worldHeight);
godRays.color = 0xffffff;
godRays.speed = 0.1;  // animation speed
godRays.size = 0.1;   // number of rays ~= 1 / size
godRays.skew = -0.3;     // angle of rays
godRays.shear = 0.3;    // spread of angle
//godRays.fade = 0.5;     // rays fade out towards the bottom
//godRays.contrast = .5; // distinctiveness and brightness of the rays
 
addChild(godRays);
fxJuggler.add(godRays);

@tadeubas
Copy link

Solved! Just increase contrast a little 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment