raindrop-fx
Version:
Rain drop effect with WebGL
473 lines (419 loc) • 17.9 kB
text/typescript
import
{
Blending, Color, DepthTest, mat4, MaterialFromShader, MeshBuilder, quat, RenderBuffer,
RenderTarget, RenderTexture, Shader, shaderProp, SimpleTexturedMaterial, Texture, Texture2D, TextureData,
TextureFormat, TextureImporter, Utils, vec2, vec4, WrapMode, ZograRenderer
} from "@sardinefish/zogra-renderer";
import raindropTexture from "../assets/img/raindrop.png";
import { BlurRenderer } from "./blur";
import { RainDrop } from "./raindrop";
import { randomRange } from "./random";
import defaultFrag from "./shader/2d-frag.glsl";
import defaultVert from "./shader/2d-vert.glsl";
import mistBackground from "./shader/bg-mist.glsl";
import composeFrag from "./shader/compose.glsl";
import dropletVert from "./shader/droplet-vert.glsl";
import dropletFrag from "./shader/droplet.glsl";
import raindropErase from "./shader/erase.glsl";
import raindropFrag from "./shader/raindrop-frag.glsl";
import raindropVert from "./shader/raindrop-vert.glsl";
import { Time } from "./utils";
class RaindropMaterial extends MaterialFromShader(new Shader(raindropVert, raindropFrag, {
blendRGB: [Blending.OneMinusDstColor, Blending.OneMinusSrcColor],
depth: DepthTest.Disable,
zWrite: false,
attributes: {
size: "aSize",
modelMatrix: "aModelMatrix",
}
}))
{
texture: Texture | null = null;
size: number = 0;
}
class DropletMaterial extends MaterialFromShader(new Shader(dropletVert, dropletFrag, {
blendRGB: [Blending.OneMinusDstColor, Blending.OneMinusSrcColor],
depth: DepthTest.Disable,
zWrite: false,
attributes: {
size: "aSize",
modelMatrix: "aModelMatrix",
}
}))
{
texture: Texture | null = null;
spawnRect: vec4 = vec4(0, 0, 1, 1);
sizeRange: vec2 = vec2(10, 20);
seed: number = 1;
}
class FinalCompose extends MaterialFromShader(new Shader(defaultVert, composeFrag, {
blend: [Blending.SrcAlpha, Blending.OneMinusSrcAlpha],
depth: DepthTest.Disable,
zWrite: false
}))
{
background: Texture | null = null;
backgroundSize: vec4 = vec4.one();
raindropTex: Texture | null = null;
dropletTex: Texture | null = null;
mistTex: Texture | null = null;
smoothRaindrop: vec2 = vec2(0.95, 1.0);
refractParams: vec2 = vec2(0.4, 0.6);
lightPos: vec4 = vec4(.5, .5, 2, 1);
diffuseLight: Color = new Color(0.3, 0.3, 0.3, 0.8);
specularParams: vec4 = vec4(1, 1, 1, 32);
bump: number = 1;
}
class RaindropErase extends SimpleTexturedMaterial(new Shader(defaultVert, raindropErase, {
// blend: [Blending.Zero, Blending.OneMinusSrcAlpha],
blendRGB: [Blending.Zero, Blending.OneMinusSrcAlpha],
blendAlpha: [Blending.Zero, Blending.OneMinusSrcAlpha],
}))
{
eraserSize: vec2 = vec2(0.93, 1.0);
}
const MistAccumulate = SimpleTexturedMaterial(new Shader(defaultVert, defaultFrag, {
blend: [Blending.One, Blending.One]
}));
class MistBackgroundCompose extends SimpleTexturedMaterial(new Shader(defaultVert, mistBackground, {
blend: [Blending.SrcAlpha, Blending.OneMinusSrcAlpha]
}))
{
mistColor: Color = new Color(0.01, 0.01, 0.01, 1);
mistTex: Texture | null = null;
}
export interface RenderOptions
{
canvas: HTMLCanvasElement;
width: number;
height: number;
background: TextureData | string;
/**
* Background blur steps used for background & raindrop refract image.
* Value should be integer from 0 to log2(backgroundSize).
* Recommend 3 or 4
*/
backgroundBlurSteps: number;
/**
* Enable blurry mist effect
*/
mist: boolean;
/**
* [r, g, b, a] mist color, each component in range (0..1).
* The alpha is used for whole mist layer.
* Recommend [0.01, 0.01, 0.01, 1]
*/
mistColor: [number, number, number, number];
/**
* Describe how long takes mist alpha from 0 to 1
*/
mistTime: number;
/**
* Background blur steps used for mist.
* Value should be integer from 0 to log2(backgroundSize).
* Recommended value = backgroundBlurSteps + 1
*/
mistBlurStep: number;
/**
* Tiny droplet spawn rate.
*/
dropletsPerSeconds: number;
/**
* Random size range of tiny drplets.
* Recommend [10, 30]
*/
dropletSize: [number, number];
/**
* Smooth range [a, b] of raindrop edge.
* Recommend [0.96, 1.0]
*
* Larger range of (b - a) have smoother edge.
*
* Lower value b makes raindrop larger with a distort edge
*/
smoothRaindrop: [number, number];
/**
* Refract uv scale of minimal raindrop.
* Recommend in range (0.2, 0.6)
*/
refractBase: number,
/**
* Refract uv scaled by raindrop size.
* Rocommend in range (0.4, 0.8)
*/
refractScale: number,
/**
* Describe how raindrops are composed.
*
* - `smoother` compose raindrops normal with 'exclusion' blend mode
*
* - `harder` compose raindrops normal with 'normal' blend mode
*/
raindropCompose: "smoother" | "harder"
/**
* Screen space (0..1) light direction or position.
* Recommend [-1, 1, 2, 0]
*
* - Use [x, y, z, 0] to indicate a direction
*
* - Use [x, y, z, 1] to indicate a position
*/
raindropLightPos: [number, number, number, number];
/**
* Lambertian diffuse lighting Color.
* Recommend [0.3, 0.3, 0.3]
*/
raindropDiffuseLight: [number, number, number];
/**
* Higher value makes more shadow.
* Recommend in range (0.6..0.8)
*/
raindropShadowOffset: number;
/**
* Similar to `smoothRaindrop`. Used to erase droplets & mist.
* Recommended value [0.93, 1.0]
*/
raindropEraserSize: [number, number];
/**
* Specular light clor.
* Recommend disable it with [0, 0, 0] :)
*/
raindropSpecularLight: [number, number, number];
/**
* Blinn-Phong exponent representing shininess.
* Value from 0 to 1024 is ok
*/
raindropSpecularShininess: number;
/**
* Will apply to calculate screen space normal for lighting.
* Larger value makes raindrops looks more flat.
* Recommend in range (0.3..1)
*/
raindropLightBump: number,
}
export class RaindropRenderer
{
renderer: ZograRenderer;
options: RenderOptions;
private raindropTex: Texture2D = null as any;
private originalBackground: Texture2D = null as any;
private background: RenderTexture;
private raindropComposeTex: RenderTexture;
private dropletTexture: RenderTexture;
private mistTexture: RenderTexture;
private blurryBackground: RenderTexture;
private mistBackground: RenderTexture;
private blurRenderer: BlurRenderer;
private matrlCompose = new FinalCompose();
private matrlRaindrop = new RaindropMaterial();
private matrlDroplet = new DropletMaterial();
private matrlErase = new RaindropErase();
private matrlMist = new MistAccumulate();
private matrlMistCompose = new MistBackgroundCompose();
private projectionMatrix: mat4;
private mesh = MeshBuilder.quad();
private raindropBuffer = new RenderBuffer({
size: "float",
modelMatrix: "mat4",
}, 3000);
// deubg: DebugLayerRenderer = new DebugLayerRenderer();
constructor(options: RenderOptions)
{
this.renderer = new ZograRenderer(options.canvas);
this.renderer.gl.getExtension("EXT_color_buffer_float");
this.options = options;
this.projectionMatrix = mat4.ortho(0, options.width, 0, options.height, 1, -1);
this.raindropComposeTex = new RenderTexture(options.width, options.height, false);
this.background = new RenderTexture(options.width, options.height, false);
this.dropletTexture = new RenderTexture(options.width, options.height, false);
this.blurryBackground = new RenderTexture(options.width, options.height, false);
this.mistBackground = new RenderTexture(options.width, options.height, false);
this.mistTexture = new RenderTexture(options.width, options.height, false, TextureFormat.R16F);
this.blurRenderer = new BlurRenderer(this.renderer);
this.renderer.setViewProjection(mat4.identity(), this.projectionMatrix);
}
async loadAssets()
{
// this.renderer.blit(null, RenderTarget.CanvasTarget);
this.raindropTex = await TextureImporter
.buffer(raindropTexture)
.then(t=>t.tex2d());
this.matrlRaindrop.texture = this.raindropTex;
this.matrlDroplet.texture = this.raindropTex;
await this.reloadBackground();
}
async reloadBackground()
{
this.originalBackground?.destroy();
if (typeof (this.options.background) === "string")
{
this.originalBackground = await TextureImporter
.url(this.options.background)
.then(t => t.tex2d({ wrapMpde: WrapMode.Clamp }));
this.originalBackground.wrapMode = WrapMode.Clamp;
this.originalBackground.updateParameters();
}
else
{
this.originalBackground = new Texture2D();
this.originalBackground.setData(this.options.background);
this.originalBackground.updateParameters();
}
const [srcRect, dstRect] = Utils.imageResize(this.originalBackground.size, this.background.size, Utils.ImageSizing.Cover);
this.renderer.blit(this.originalBackground, this.background, this.renderer.assets.materials.blitCopy, srcRect, dstRect);
this.background.generateMipmap();
this.blurBackground();
}
resize()
{
this.renderer.setSize(this.options.width, this.options.height);
this.projectionMatrix = mat4.ortho(0, this.options.width, 0, this.options.height, 1, -1);
this.renderer.setViewProjection(mat4.identity(), this.projectionMatrix);
this.raindropComposeTex.resize(this.options.width, this.options.height);
this.background.resize(this.options.width, this.options.height);
this.dropletTexture.resize(this.options.width, this.options.height);
this.blurryBackground.resize(this.options.width, this.options.height);
this.mistBackground.resize(this.options.width, this.options.height);
this.mistTexture.resize(this.options.width, this.options.height);
const [srcRect, dstRect] = Utils.imageResize(this.originalBackground.size, this.background.size, Utils.ImageSizing.Cover);
this.renderer.blit(this.originalBackground, this.background, this.renderer.assets.materials.blitCopy, srcRect, dstRect);
this.background.generateMipmap();
this.blurBackground();
}
render(raindrops: RainDrop[], time: Time)
{
this.drawDroplet(time);
this.drawMist(time.dt);
this.drawRaindrops(raindrops);
this.renderer.setRenderTarget(RenderTarget.CanvasTarget);
this.renderer.clear(Color.black);
this.drawBackground();
this.matrlCompose.background = this.blurryBackground;
this.matrlCompose.backgroundSize = vec4(this.options.width, this.options.height, 1 / this.options.width, 1 / this.options.height);
this.matrlCompose.raindropTex = this.raindropComposeTex;
this.matrlCompose.dropletTex = this.dropletTexture;
this.matrlCompose.mistTex = this.mistTexture;
this.matrlCompose.smoothRaindrop = vec2(...this.options.smoothRaindrop);
this.matrlCompose.refractParams = vec2(this.options.refractBase, this.options.refractScale);
this.matrlCompose.lightPos = vec4(...this.options.raindropLightPos);
this.matrlCompose.diffuseLight = new Color(...this.options.raindropDiffuseLight, this.options.raindropShadowOffset);
this.matrlCompose.specularParams = vec4(...this.options.raindropSpecularLight, this.options.raindropSpecularShininess);
this.matrlCompose.bump = this.options.raindropLightBump;
this.renderer.blit(null, RenderTarget.CanvasTarget, this.matrlCompose);
}
private blurBackground()
{
// Blur background with N steps downsample + N steps upsample
if (!this.options.mist)
{
this.blurRenderer.blur(this.background, this.options.backgroundBlurSteps, this.blurryBackground);
}
else
{
// Downsample to max steps
// Then upsample from a larger texture before smaller texture to avoid override
this.blurRenderer.init(this.background);
this.blurRenderer.downSample(this.background, Math.max(this.options.backgroundBlurSteps, this.options.mistBlurStep));
if (this.options.backgroundBlurSteps === this.options.mistBlurStep)
{
this.blurRenderer.upSample(this.options.backgroundBlurSteps, this.blurryBackground);
this.renderer.blit(this.blurryBackground, this.mistBackground);
}
else if (this.options.mistBlurStep > this.options.backgroundBlurSteps)
{
this.blurRenderer.upSample(this.options.backgroundBlurSteps, this.blurryBackground);
this.blurRenderer.upSample(this.options.mistBlurStep, this.mistBackground);
}
else
{
this.blurRenderer.upSample(this.options.mistBlurStep, this.mistBackground);
this.blurRenderer.upSample(this.options.backgroundBlurSteps, this.blurryBackground);
}
}
}
private drawMist(dt: number)
{
if (!this.options.mist)
return;
this.matrlMist.color.r = dt / this.options.mistTime;
this.renderer.blit(this.renderer.assets.textures.default, this.mistTexture, this.matrlMist);
}
private drawBackground()
{
this.renderer.blit(this.blurryBackground, RenderTarget.CanvasTarget);
if (this.options.mist)
{
this.matrlMistCompose.mistTex = this.mistTexture;
this.matrlMistCompose.texture = this.mistBackground;
this.matrlMistCompose.mistColor = new Color(...this.options.mistColor);
this.renderer.blit(this.mistBackground, RenderTarget.CanvasTarget, this.matrlMistCompose);
}
}
private drawRaindrops(raindrops: RainDrop[])
{
if (raindrops.length > this.raindropBuffer.length)
this.raindropBuffer.resize(this.raindropBuffer.length * 2);
this.renderer.setRenderTarget(this.raindropComposeTex);
this.renderer.clear(Color.black.transparent());
for (let i = 0; i < raindrops.length; i++)
{
const raindrop = raindrops[i];
const model = mat4.rts(quat.identity(), raindrop.pos.toVec3(), raindrop.size.toVec3(1));
this.raindropBuffer[i].modelMatrix.set(model);
this.raindropBuffer[i].size[0] = raindrop.size.x / 100;
}
switch (this.options.raindropCompose)
{
case "smoother":
this.matrlRaindrop.shader.setPipelineStates({
blendRGB: [Blending.OneMinusDstColor, Blending.OneMinusSrcColor]
});
this.matrlDroplet.shader.setPipelineStates({
blendRGB: [Blending.OneMinusDstColor, Blending.OneMinusSrcColor],
});
break;
case "harder":
this.matrlRaindrop.shader.setPipelineStates({
blendRGB: [Blending.One, Blending.OneMinusSrcAlpha]
});
this.matrlDroplet.shader.setPipelineStates({
blendRGB: [Blending.One, Blending.OneMinusSrcAlpha],
});
break;
}
this.renderer.drawMeshInstance(this.mesh, this.raindropBuffer, this.matrlRaindrop, raindrops.length);
this.matrlErase.eraserSize = vec2(...this.options.raindropEraserSize);
this.renderer.blit(this.raindropComposeTex, this.dropletTexture, this.matrlErase);
if (this.options.mist)
this.renderer.blit(this.raindropComposeTex, this.mistTexture, this.matrlErase);
}
private drawDroplet(time: Time)
{
this.renderer.setRenderTarget(this.dropletTexture);
const count = this.options.dropletsPerSeconds * time.dt;
this.matrlDroplet.spawnRect = vec4(0, 0, this.options.width, this.options.height);
this.matrlDroplet.sizeRange = vec2(...this.options.dropletSize);
this.matrlDroplet.seed = randomRange(0, 133);
this.renderer.drawMeshProceduralInstance(this.mesh, this.matrlDroplet, count);
}
}