diff --git a/src/entities/CameraEntity.ts b/src/entities/CameraEntity.ts index 876eb73..273d61a 100644 --- a/src/entities/CameraEntity.ts +++ b/src/entities/CameraEntity.ts @@ -17,6 +17,8 @@ export interface CameraEntityOptions { root: Entity; target: RenderTarget; lights: LightEntity[]; + textureIBLLUT: GLCatTexture; + textureEnv: GLCatTexture; textureRandom: GLCatTexture; } @@ -185,6 +187,8 @@ export class CameraEntity { shadingMaterial.blend = [ gl.ONE, gl.ONE ]; shadingMaterial.addUniformTexture( 'samplerAo', aoTarget.texture ); shadingMaterial.addUniformTexture( 'samplerShadow', light.shadowMap.texture ); + shadingMaterial.addUniformTexture( 'samplerIBLLUT', options.textureIBLLUT ); + shadingMaterial.addUniformTexture( 'samplerEnv', options.textureEnv ); shadingMaterial.addUniformTexture( 'samplerRandom', options.textureRandom ); const shadingQuad = new Quad( { diff --git a/src/entities/EnvironmentMap.ts b/src/entities/EnvironmentMap.ts new file mode 100644 index 0000000..ad95171 --- /dev/null +++ b/src/entities/EnvironmentMap.ts @@ -0,0 +1,81 @@ +import { Entity } from '../heck/Entity'; +import { GLCatTexture } from '@fms-cat/glcat-ts'; +import { Material } from '../heck/Material'; +import { Quad } from '../heck/components/Quad'; +import environmentMapFrag from '../shaders/environment-map.frag'; +import quadVert from '../shaders/quad.vert'; +import { BufferRenderTarget } from '../heck/BufferRenderTarget'; +import { Swap, Xorshift } from '@fms-cat/experimental'; +import { Lambda } from '../heck/components/Lambda'; + +const WIDTH = 1024; +const HEIGHT = 512; + +export class EnvironmentMap { + public entity: Entity; + + public swap: Swap; + + public get texture(): GLCatTexture { + return this.swap.o.texture; + } + + public constructor() { + this.entity = new Entity(); + this.entity.visible = false; + + const rng = new Xorshift( 114514 ); + + // -- swap ------------------------------------------------------------------------------------- + this.swap = new Swap( + new BufferRenderTarget( { + width: WIDTH, + height: HEIGHT, + name: process.env.DEV && 'EnvironmentMap/swap0', + } ), + new BufferRenderTarget( { + width: WIDTH, + height: HEIGHT, + name: process.env.DEV && 'EnvironmentMap/swap1', + } ), + ); + + // -- post ------------------------------------------------------------------------------------- + const material = new Material( + quadVert, + environmentMapFrag, + ); + material.addUniform( 'uniformSeed', '4f', rng.gen(), rng.gen(), rng.gen(), rng.gen() ); + material.addUniformTexture( 'sampler0', this.swap.i.texture ); + + if ( process.env.DEV ) { + if ( module.hot ) { + module.hot.accept( '../shaders/environment-map.frag', () => { + material.replaceShader( quadVert, environmentMapFrag ); + } ); + } + } + + const quad = new Quad( { + target: this.swap.o, + material, + name: process.env.DEV && 'EnvironmentMap/quad', + } ); + + // -- swapper ---------------------------------------------------------------------------------- + this.entity.components.push( new Lambda( { + onUpdate: () => { + this.swap.swap(); + + material.addUniform( 'uniformSeed', '4f', rng.gen(), rng.gen(), rng.gen(), rng.gen() ); + material.addUniformTexture( 'sampler0', this.swap.i.texture ); + + quad.target = this.swap.o; + }, + visible: false, + name: process.env.DEV && 'EnvironmentMap/swapper', + } ) ); + + this.entity.components.push( quad ); + } +} diff --git a/src/entities/IBLLUT.ts b/src/entities/IBLLUT.ts new file mode 100644 index 0000000..287470d --- /dev/null +++ b/src/entities/IBLLUT.ts @@ -0,0 +1,80 @@ +import { Entity } from '../heck/Entity'; +import { GLCatTexture } from '@fms-cat/glcat-ts'; +import { Material } from '../heck/Material'; +import { Quad } from '../heck/components/Quad'; +import iblLutFrag from '../shaders/ibl-lut.frag'; +import quadVert from '../shaders/quad.vert'; +import { BufferRenderTarget } from '../heck/BufferRenderTarget'; +import { Swap } from '@fms-cat/experimental'; +import { Lambda } from '../heck/components/Lambda'; +import { vdc } from '../utils/vdc'; + +const IBL_SIZE = 256; + +export class IBLLUT { + public entity: Entity; + + public swap: Swap; + + public get texture(): GLCatTexture { + return this.swap.o.texture; + } + + public constructor() { + this.entity = new Entity(); + this.entity.visible = false; + + // -- swap ------------------------------------------------------------------------------------- + this.swap = new Swap( + new BufferRenderTarget( { + width: IBL_SIZE, + height: IBL_SIZE, + name: process.env.DEV && 'IBLLUT/swap0', + } ), + new BufferRenderTarget( { + width: IBL_SIZE, + height: IBL_SIZE, + name: process.env.DEV && 'IBLLUT/swap1', + } ), + ); + + // -- post ------------------------------------------------------------------------------------- + let samples = 0.0; + + const material = new Material( + quadVert, + iblLutFrag, + ); + material.addUniform( 'samples', '1f', samples ); + material.addUniform( 'vdc', '1f', vdc( samples, 2.0 ) ); + material.addUniformTexture( 'sampler0', this.swap.i.texture ); + + const quad = new Quad( { + target: this.swap.o, + material, + name: process.env.DEV && 'IBLLUT/quad', + } ); + + // -- swapper ---------------------------------------------------------------------------------- + this.entity.components.push( new Lambda( { + onUpdate: () => { + samples ++; + this.swap.swap(); + + if ( samples > 1024 ) { + this.entity.active = false; + } else { + material.addUniform( 'samples', '1f', samples ); + material.addUniform( 'vdc', '1f', vdc( samples, 2.0 ) ); + material.addUniformTexture( 'sampler0', this.swap.i.texture ); + + quad.target = this.swap.o; + } + }, + visible: false, + name: process.env.DEV && 'IBLLUT/swapper', + } ) ); + + this.entity.components.push( quad ); + } +} diff --git a/src/main.ts b/src/main.ts index 246d83e..5bd7edd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,6 +24,8 @@ import { RTInspector } from './entities/RTInspector'; import { Component } from './heck/components/Component'; import { FlickyParticles } from './entities/FlickyParticles'; import { PixelSorter } from './entities/PixelSorter'; +import { IBLLUT } from './entities/IBLLUT'; +import { EnvironmentMap } from './entities/EnvironmentMap'; // == music ======================================================================================== const audio = new AudioContext(); @@ -193,6 +195,13 @@ dog.root.components.push( new Lambda( { name: process.env.DEV && 'main/update', } ) ); +// -- bake ----------------------------------------------------------------------------------------- +const ibllut = new IBLLUT(); +dog.root.children.push( ibllut.entity ); + +const environmentMap = new EnvironmentMap(); +dog.root.children.push( environmentMap.entity ); + // -- "objects" ------------------------------------------------------------------------------------ const sphereParticles = new SphereParticles( { particlesSqrt: 256, @@ -201,13 +210,13 @@ const sphereParticles = new SphereParticles( { } ); dog.root.children.push( sphereParticles.entity ); -const trails = new Trails( { - trails: 4096, - trailLength: 64, - textureRandom: randomTexture.texture, - textureRandomStatic: randomTextureStatic.texture -} ); -dog.root.children.push( trails.entity ); +// const trails = new Trails( { +// trails: 4096, +// trailLength: 64, +// textureRandom: randomTexture.texture, +// textureRandomStatic: randomTextureStatic.texture +// } ); +// dog.root.children.push( trails.entity ); const rings = new Rings(); dog.root.children.push( rings.entity ); @@ -219,11 +228,11 @@ const flickyParticles = new FlickyParticles( { } ); dog.root.children.push( flickyParticles.entity ); -const raymarcher = new Raymarcher( { - textureRandom: randomTexture.texture, - textureRandomStatic: randomTextureStatic.texture -} ); -dog.root.children.push( raymarcher.entity ); +// const raymarcher = new Raymarcher( { +// textureRandom: randomTexture.texture, +// textureRandomStatic: randomTextureStatic.texture +// } ); +// dog.root.children.push( raymarcher.entity ); // -- things that is not an "object" --------------------------------------------------------------- const swapOptions = { @@ -249,7 +258,7 @@ const light = new LightEntity( { shadowMapFar: 20.0, namePrefix: process.env.DEV && 'light1', } ); -light.color = [ 60.0, 60.0, 60.0 ]; +light.color = [ 40.0, 40.0, 40.0 ]; light.entity.transform.lookAt( new Vector3( [ -1.0, 2.0, 8.0 ] ) ); dog.root.children.push( light.entity ); @@ -271,6 +280,8 @@ const camera = new CameraEntity( { light, // light2 ], + textureIBLLUT: ibllut.texture, + textureEnv: environmentMap.texture, textureRandom: randomTexture.texture } ); camera.camera.clear = [ 0.0, 0.0, 0.0, 0.0 ]; diff --git a/src/shaders/bloom-post.frag b/src/shaders/bloom-post.frag index 8837b95..1cf798b 100644 --- a/src/shaders/bloom-post.frag +++ b/src/shaders/bloom-post.frag @@ -9,12 +9,16 @@ out vec4 fragColor; uniform sampler2D samplerDry; uniform sampler2D samplerWet; +vec4 sampleLOD( vec2 uv, float lv ) { + float p = pow( 0.5, float( lv ) ); + vec2 uvt = mix( vec2( 1.0 - p ), vec2( 1.0 - 0.5 * p ), uv ); + return texture( samplerWet, uvt ); +} + void main() { fragColor = texture( samplerDry, vUv ); for ( int i = 0; i < 5; i ++ ) { - float fuck = pow( 0.5, float( i ) ); - vec2 suv = mix( vec2( 1.0 - fuck ), vec2( 1.0 - 0.5 * fuck ), vUv ); - fragColor += texture( samplerWet, suv ); + fragColor += sampleLOD( vUv, float( i ) ); } fragColor.xyz = max( vec3( 0.0 ), fragColor.xyz ); } diff --git a/src/shaders/environment-map.frag b/src/shaders/environment-map.frag new file mode 100644 index 0000000..fc18873 --- /dev/null +++ b/src/shaders/environment-map.frag @@ -0,0 +1,119 @@ +#version 300 es + +// https://learnopengl.com/PBR/IBL/Specular-IBL + +precision highp float; + +#define saturate(x) clamp(x,0.,1.) +#define linearstep(a,b,x) saturate(((x)-(a))/((b)-(a))) + +#pragma glslify: prng = require( ./-prng ); + +const int SAMPLES = 16; +const float UV_MARGIN = 0.9; +const float PI = 3.14159265; +const float TAU = 6.283185307; + +in vec2 vUv; + +out vec4 fragColor; + +uniform float head; +uniform vec2 resolution; +uniform vec4 uniformSeed; +uniform sampler2D sampler0; + +vec4 seed; + +float vdc( float i, float base ) { + float r = 0.0; + float denom = 1.0; + + for ( int j = 0; j < 32; j ++ ) { + denom *= base; + r += mod( i, base ) / denom; + i = floor( i / base ); + + if ( i <= 0.0 ) { break; } + } + + return r; +} + +vec3 ImportanceSampleGGX( vec2 Xi, float roughness, vec3 N ) { + float a = roughness * roughness; + + float phi = TAU * Xi.x; + float cosTheta = roughness < 0.0 // negative roughness to usa lambert ??? + ? asin( sqrt( Xi.y ) ) + : sqrt( ( 1.0 - Xi.y ) / ( 1.0 + ( a * a - 1.0 ) * Xi.y ) ); + float sinTheta = sqrt( 1.0 - cosTheta * cosTheta ); + + // from spherical coordinates to cartesian coordinates + vec3 H = vec3( + cos( phi ) * sinTheta, + sin( phi ) * sinTheta, + cosTheta + ); + + // from tangent-space vector to world-space sample vector + vec3 up = abs( N.y ) < 0.999 ? vec3( 0.0, 1.0, 0.0 ) : vec3( 1.0, 0.0, 0.0 ); + vec3 tangent = normalize( cross( up, N ) ); + vec3 bitangent = cross( N, tangent ); + + vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z; + return normalize( sampleVec ); +} + +vec3 haha( vec3 L ) { + bool circ = dot( L, normalize( vec3( 1.0, 3.0, 3.0 ) ) ) > 0.9; + vec3 c = circ ? 0.01 * vec3( 0.1, 0.1, 1.0 ) : vec3( 0.0 ); + + bool ring = abs( dot( L, vec3( 0.0, 1.0, 0.0 ) ) ) < 0.1; + c += ring ? 10.0 * vec3( 0.1, 1.0, 0.3 ) : vec3( 0.0 ); + + return c; + // return 0.5 + 0.5 * L; +} + +void main() { + vec2 halfTexel = 1.0 / resolution; // * 0.5; + + float lv = ceil( -log( 1.0 - min( vUv.x, vUv.y ) ) / log( 2.0 ) ); + float p = pow( 0.5, lv ); + vec2 uv00 = floor( vUv / p ) * p; + vec2 uv11 = uv00 + p; + vec2 uv = clamp( vUv, uv00 + halfTexel, uv11 - halfTexel ); + uv = linearstep( uv00, uv11, uv ); + uv = ( uv - 0.5 ) / UV_MARGIN + 0.5; + + vec3 tex = texture( sampler0, vUv ).xyz; + float roughness = lv * 0.2; + + float a = TAU * uv.x; + float b = PI * ( uv.y - 0.5 ); + vec3 N = vec3( sin( a ) * cos( b ), -sin( b ), -cos( a ) * cos( b ) ); + vec3 R = N; + vec3 V = R; + + seed = uniformSeed + N.xyzx; + + vec4 col = vec4( 0.0 ); + for ( int i = 0; i < SAMPLES; i ++ ) { + vec2 Xi = vec2( prng( seed ), prng( seed ) ); + vec3 H = ImportanceSampleGGX( Xi, roughness, N ); + vec3 L = normalize( 2.0 * dot( V, H ) * H - V ); + + float NoL = dot( N, L ); + + if ( NoL > 0.0 ) { + col += vec4( haha( L ), 1.0 ) * NoL; + } + } + + col.xyz = col.w <= 0.001 ? vec3( 0.0 ) : ( col.xyz / col.w ); + + tex.xyz = mix( tex.xyz, col.xyz, 1.0 / 16.0 ); + + fragColor = vec4( tex, 1.0 ); +} diff --git a/src/shaders/ibl-lut.frag b/src/shaders/ibl-lut.frag new file mode 100644 index 0000000..fdf6b5c --- /dev/null +++ b/src/shaders/ibl-lut.frag @@ -0,0 +1,90 @@ +#version 300 es + +precision highp float; + +const float TAU = 6.283185307; + +in vec2 vUv; + +out vec4 fragColor; + +uniform float samples; +uniform float vdc; +uniform sampler2D sampler0; + +vec3 ImportanceSampleGGX( vec2 Xi, float roughness, vec3 N ) { + float a = roughness * roughness; + + float phi = TAU * Xi.x; + float cosTheta = sqrt( ( 1.0 - Xi.y ) / ( 1.0 + ( a * a - 1.0 ) * Xi.y ) ); + float sinTheta = sqrt( 1.0 - cosTheta * cosTheta ); + + // from spherical coordinates to cartesian coordinates + vec3 H = vec3( + cos( phi ) * sinTheta, + sin( phi ) * sinTheta, + cosTheta + ); + + // from tangent-space vector to world-space sample vector + vec3 up = abs( N.y ) < 0.999 ? vec3( 0.0, 1.0, 0.0 ) : vec3( 1.0, 0.0, 0.0 ); + vec3 tangent = normalize( cross( up, N ) ); + vec3 bitangent = cross( N, tangent ); + + vec3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z; + return normalize( sampleVec ); +} + +float GeometrySchlickGGX( float NdotV, float roughness ) { + float a = roughness; + float k = ( a * a ) / 2.0; + + float nom = NdotV; + float denom = NdotV * ( 1.0 - k ) + k; + + return nom / denom; +} + +float GeometrySmith( float roughness, float NoV, float NoL ) { + float ggx2 = GeometrySchlickGGX( NoV, roughness ); + float ggx1 = GeometrySchlickGGX( NoL, roughness ); + + return ggx1 * ggx2; +} + +// https://github.com/HectorMF/BRDFGenerator/blob/master/BRDFGenerator/BRDFGenerator.cpp +vec2 IntegrateBRDF( float NdotV, float roughness ) { + vec3 V = vec3( sqrt( 1.0 - NdotV * NdotV ), 0.0, NdotV ); + vec3 N = vec3( 0.0, 0.0, 1.0 ); + + vec2 Xi = vec2( samples / 1024.0, vdc ); + vec3 H = ImportanceSampleGGX( Xi, roughness, N ); + vec3 L = normalize( 2.0 * dot( V, H ) * H - V ); + + float NoL = max( L.z, 0.0 ); + float NoH = max( H.z, 0.0 ); + float VoH = max( dot( V, H ), 0.0 ); + float NoV = max( dot( N, V ), 0.0 ); + + if ( NoL > 0.0 ) { + float G = GeometrySmith( roughness, NoV, NoL ); + + float G_Vis = ( G * VoH ) / ( NoH * NoV ); + float Fc = pow( 1.0 - VoH, 5.0 ); + + return vec2( ( 1.0 - Fc ) * G_Vis, Fc * G_Vis ); + } + + return vec2( 0.0 ); +} + +void main() { + vec2 tex = texture( sampler0, vUv ).xy; + + float NdotV = vUv.y; + float roughness = vUv.x; + + tex = mix( tex, IntegrateBRDF( NdotV, roughness ), 1.0 / samples ); + + fragColor = vec4( tex, 0.0, 1.0 ); +} diff --git a/src/shaders/raymarcher.frag b/src/shaders/raymarcher.frag index d67f59d..3ee3244 100644 --- a/src/shaders/raymarcher.frag +++ b/src/shaders/raymarcher.frag @@ -70,7 +70,7 @@ void main() { } vec3 normal = normalFunc( rayPos, 1E-4 ); - vec4 color = vec4( 0.1, 0.2, 0.4, 1.0 ); + vec4 color = vec4( 0.4, 0.7, 0.9, 1.0 ); vec4 projPos = projectionMatrix * viewMatrix * vec4( rayPos, 1.0 ); // terrible float depth = projPos.z / projPos.w; @@ -79,5 +79,5 @@ void main() { fragPosition = vec4( rayPos, depth ); fragNormal = vec4( normal, 1.0 ); fragColor = color; - fragWTF = vec4( vec3( 2.0, 0.9, 0.9 ), MTL_IRIDESCENT ); + fragWTF = vec4( vec3( 0.8, 0.8, 0.0 ), MTL_PBR ); } diff --git a/src/shaders/shading.frag b/src/shaders/shading.frag index fc372f1..534a99e 100644 --- a/src/shaders/shading.frag +++ b/src/shaders/shading.frag @@ -8,6 +8,7 @@ const int MTL_PBR = 2; const int MTL_GRADIENT = 3; const int MTL_IRIDESCENT = 4; const int AO_ITER = 8; +const float ENV_UV_MARGIN = 0.9; const float AO_BIAS = 0.0; const float AO_RADIUS = 0.5; const float PI = 3.14159265359; @@ -36,7 +37,7 @@ uniform sampler2D sampler1; // normal.xyz (yes, this is not good) uniform sampler2D sampler2; // color.rgba (what is a though????) uniform sampler2D sampler3; // materialParams.xyz, materialId uniform sampler2D samplerShadow; -uniform sampler2D samplerBRDFLUT; +uniform sampler2D samplerIBLLUT; uniform sampler2D samplerEnv; uniform sampler2D samplerAo; uniform sampler2D samplerRandom; @@ -86,6 +87,21 @@ vec3 blurpleGradient( float t ) { ); } +vec4 sampleEnvNearest( vec2 uv, float lv ) { + float p = pow( 0.5, float( lv ) ); + vec2 uvt = ENV_UV_MARGIN * ( uv - 0.5 ) + 0.5; + uvt = mix( vec2( 1.0 - p ), vec2( 1.0 - 0.5 * p ), uvt ); + return texture( samplerEnv, uvt ); +} + +vec4 sampleEnvLinear( vec2 uv, float lv ) { + return mix( + sampleEnvNearest( uv, floor( lv ) ), + sampleEnvNearest( uv, floor( lv + 1.0 ) ), + fract( lv ) + ); +} + // == structs ====================================================================================== struct Isect { vec2 screenUv; @@ -188,16 +204,18 @@ vec3 shadePBR( Isect isect, AngularInfo aI ) { vec3 color = shade; #ifdef IS_FIRST_LIGHT -// vec3 refl = reflect( aI.V, isect.normal ); -// vec2 envCoord = vec2( -// 0.5 + atan( refl.z, refl.x ) / TAU, -// 0.5 + atan( refl.y, length( refl.zx ) ) / PI -// ); -// vec2 brdf = texture( samplerBRDFLUT, vec2( aI.dotNV, 1.0 - roughness ) ).xy; + vec3 refl = reflect( aI.V, isect.normal ); + vec2 envUv = vec2( + 0.5 + atan( refl.x, -refl.z ) / TAU, + 0.5 + atan( refl.y, length( refl.zx ) ) / PI + ); -// vec3 texEnv = 0.2 * pow( texture( samplerEnv, envCoord ).rgb, vec3( 2.2 ) ); -// color += PI * texEnv * ( brdf.x * F0 + brdf.y ); + // reflective ibl + vec2 brdf = texture( samplerIBLLUT, vec2( aI.dotNV, 1.0 - roughness ) ).xy; + vec3 texEnv = 0.2 * sampleEnvLinear( envUv, 5.0 * roughness ).rgb; + color += PI * ao * texEnv * ( brdf.x * F0 + brdf.y ); + // emissive color += emissive * aI.dotNV * isect.albedo; #endif // IS_FIRST_LIGHT diff --git a/src/shaders/sphere-particles-render.frag b/src/shaders/sphere-particles-render.frag index 168af3b..eaad76f 100644 --- a/src/shaders/sphere-particles-render.frag +++ b/src/shaders/sphere-particles-render.frag @@ -38,5 +38,5 @@ void main() { fragPosition = vPosition; fragNormal = vec4( vNormal, 1.0 ); fragColor = vec4( vColor.xyz, 1.0 ); - fragWTF = vec4( vec3( 0.8, 0.8, 0.0 ), MTL_PBR ); + fragWTF = vec4( vec3( 0.4, 0.4, 0.0 ), MTL_PBR ); } diff --git a/src/shaders/sphere-particles-render.vert b/src/shaders/sphere-particles-render.vert index ee5a1be..46d76bc 100644 --- a/src/shaders/sphere-particles-render.vert +++ b/src/shaders/sphere-particles-render.vert @@ -83,15 +83,15 @@ void main() { size *= sin( PI * saturate( vLife ) ); vec3 shape = position * size; - shape.yz = rotate2D( 7.0 * vPosition.x ) * shape.yz; - shape.zx = rotate2D( 7.0 * vPosition.y ) * shape.zx; + shape.yz = rotate2D( 7.0 * ( vPosition.x + vDice.z ) ) * shape.yz; + shape.zx = rotate2D( 7.0 * ( vPosition.y + vDice.w ) ) * shape.zx; vPosition.xyz += shape; // == compute normals ============================================================================ vNormal = ( normalMatrix * vec4( normal, 1.0 ) ).xyz; - vNormal.yz = rotate2D( 7.0 * vPosition.x ) * vNormal.yz; - vNormal.zx = rotate2D( 7.0 * vPosition.y ) * vNormal.zx; + vNormal.yz = rotate2D( 7.0 * ( vPosition.x + vDice.z ) ) * vNormal.yz; + vNormal.zx = rotate2D( 7.0 * ( vPosition.y + vDice.w ) ) * vNormal.zx; // == send the vertex position =================================================================== vPosition = modelMatrix * vPosition; diff --git a/src/utils/vdc.ts b/src/utils/vdc.ts new file mode 100644 index 0000000..278c500 --- /dev/null +++ b/src/utils/vdc.ts @@ -0,0 +1,18 @@ +/** + * Generate a number using Van der Corput sequence. + * e.g. vdc(i, 2) = 1/2, 1/4, 3/4, 1/8, 5/8, 3/8, 7/8, 1/16, ... + * @param i Index of the sequence + * @param base Base of the sequence + */ +export function vdc( i: number, base: number ) { + let r = 0; + let denom = 1; + + while ( 0 < i ) { + denom *= base; + r += ( i % base ) / denom; + i = Math.floor( i / base ); + } + + return r; +}