diff --git a/examples/jsm/tsl/display/AfterImageNode.js b/examples/jsm/tsl/display/AfterImageNode.js index 5f7cb8644194b7..a2e627c054fd43 100644 --- a/examples/jsm/tsl/display/AfterImageNode.js +++ b/examples/jsm/tsl/display/AfterImageNode.js @@ -1,11 +1,18 @@ import { RenderTarget, Vector2, QuadMesh, NodeMaterial, PostProcessingUtils, TempNode, NodeUpdateType } from 'three/webgpu'; import { nodeObject, Fn, float, vec4, uv, texture, passTexture, uniform, sign, max, convertToTexture } from 'three/tsl'; +/** @module AfterImageNode **/ + const _size = /*@__PURE__*/ new Vector2(); const _quadMeshComp = /*@__PURE__*/ new QuadMesh(); let _rendererState; +/** + * Post processing node for creating an after image effect. + * + * @augments TempNode + */ class AfterImageNode extends TempNode { static get type() { @@ -14,32 +21,91 @@ class AfterImageNode extends TempNode { } + /** + * Constructs a new after image node. + * + * @param {TextureNode} textureNode - The texture node that represents the input of the effect. + * @param {Number} [damp=0.96] - The damping intensity. A higher value means a stronger after image effect. + */ constructor( textureNode, damp = 0.96 ) { super( 'vec4' ); + /** + * The texture node that represents the input of the effect. + * + * @type {TextureNode} + */ this.textureNode = textureNode; + + /** + * The texture represents the pervious frame. + * + * @type {TextureNode} + */ this.textureNodeOld = texture(); + + /** + * The damping intensity as a uniform node. + * + * @type {UniformNode} + */ this.damp = uniform( damp ); + /** + * The render target used for compositing the effect. + * + * @private + * @type {RenderTarget} + */ this._compRT = new RenderTarget( 1, 1, { depthBuffer: false } ); this._compRT.texture.name = 'AfterImageNode.comp'; + /** + * The render target that represents the previous frame. + * + * @private + * @type {RenderTarget} + */ this._oldRT = new RenderTarget( 1, 1, { depthBuffer: false } ); this._oldRT.texture.name = 'AfterImageNode.old'; + /** + * The result of the effect is represented as a separate texture node. + * + * @private + * @type {PassTextureNode} + */ this._textureNode = passTexture( this, this._compRT.texture ); + /** + * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders + * its effect once per frame in `updateBefore()`. + * + * @type {String} + * @default 'frame' + */ this.updateBeforeType = NodeUpdateType.FRAME; } + /** + * Returns the result of the effect as a texture node. + * + * @return {PassTextureNode} A texture node that represents the result of the effect. + */ getTextureNode() { return this._textureNode; } + /** + * Sets the size of the effect. + * + * @param {Number} width - The width of the effect. + * @param {Number} height - The height of the effect. + */ setSize( width, height ) { this._compRT.setSize( width, height ); @@ -47,6 +113,11 @@ class AfterImageNode extends TempNode { } + /** + * This method is used to render the effect once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ updateBefore( frame ) { const { renderer } = frame; @@ -90,6 +161,12 @@ class AfterImageNode extends TempNode { } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {PassTextureNode} + */ setup( builder ) { const textureNode = this.textureNode; @@ -141,6 +218,10 @@ class AfterImageNode extends TempNode { } + /** + * Frees internal resources. This method should be called + * when the effect is no longer required. + */ dispose() { this._compRT.dispose(); @@ -150,6 +231,14 @@ class AfterImageNode extends TempNode { } +/** + * TSL function for creating an after image node for post processing. + * + * @function + * @param {Node} node - The node that represents the input of the effect. + * @param {Number} [damp=0.96] - The damping intensity. A higher value means a stronger after image effect. + * @returns {AfterImageNode} + */ export const afterImage = ( node, damp ) => nodeObject( new AfterImageNode( convertToTexture( node ), damp ) ); export default AfterImageNode; diff --git a/examples/jsm/tsl/display/AnaglyphPassNode.js b/examples/jsm/tsl/display/AnaglyphPassNode.js index bbc5c488afa56a..ba5bb2282b98aa 100644 --- a/examples/jsm/tsl/display/AnaglyphPassNode.js +++ b/examples/jsm/tsl/display/AnaglyphPassNode.js @@ -2,6 +2,13 @@ import { Matrix3, NodeMaterial } from 'three/webgpu'; import { clamp, nodeObject, Fn, vec4, uv, uniform, max } from 'three/tsl'; import StereoCompositePassNode from './StereoCompositePassNode.js'; +/** @module AnaglyphPassNode **/ + +/** + * A render pass node that creates an anaglyph effect. + * + * @augments StereoCompositePassNode + */ class AnaglyphPassNode extends StereoCompositePassNode { static get type() { @@ -10,20 +17,43 @@ class AnaglyphPassNode extends StereoCompositePassNode { } + /** + * Constructs a new anaglyph pass node. + * + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + */ constructor( scene, camera ) { super( scene, camera ); + /** + * This flag can be used for type testing. + * + * @type {Boolean} + * @readonly + * @default true + */ this.isAnaglyphPassNode = true; // Dubois matrices from https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.7.6968&rep=rep1&type=pdf#page=4 + /** + * Color matrix node for the left eye. + * + * @type {UniformNode} + */ this._colorMatrixLeft = uniform( new Matrix3().fromArray( [ 0.456100, - 0.0400822, - 0.0152161, 0.500484, - 0.0378246, - 0.0205971, 0.176381, - 0.0157589, - 0.00546856 ] ) ); + /** + * Color matrix node for the right eye. + * + * @type {UniformNode} + */ this._colorMatrixRight = uniform( new Matrix3().fromArray( [ - 0.0434706, 0.378476, - 0.0721527, - 0.0879388, 0.73364, - 0.112961, @@ -32,6 +62,12 @@ class AnaglyphPassNode extends StereoCompositePassNode { } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {PassTextureNode} + */ setup( builder ) { const uvNode = uv(); @@ -60,4 +96,12 @@ class AnaglyphPassNode extends StereoCompositePassNode { export default AnaglyphPassNode; +/** + * TSL function for creating an anaglyph pass node. + * + * @function + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + * @returns {AnaglyphPassNode} + */ export const anaglyphPass = ( scene, camera ) => nodeObject( new AnaglyphPassNode( scene, camera ) ); diff --git a/examples/jsm/tsl/display/BleachBypass.js b/examples/jsm/tsl/display/BleachBypass.js index d3f94eaab09527..473705368c1820 100644 --- a/examples/jsm/tsl/display/BleachBypass.js +++ b/examples/jsm/tsl/display/BleachBypass.js @@ -1,5 +1,15 @@ import { float, Fn, vec3, vec4, min, max, mix, luminance } from 'three/tsl'; +/** @module BleachBypass **/ + +/** + * Applies a bleach bypass effect to the given color node. + * + * @function + * @param {Node} color - The color node to apply the sepia for. + * @param {Node} [opacity=1] - Influences how strong the effect is blended with the original color. + * @return {Node} The updated color node. + */ export const bleach = /*@__PURE__*/ Fn( ( [ color, opacity = 1 ] ) => { const base = color; diff --git a/examples/jsm/tsl/display/DepthOfFieldNode.js b/examples/jsm/tsl/display/DepthOfFieldNode.js index bd02401e822fcf..52202310d2b03f 100644 --- a/examples/jsm/tsl/display/DepthOfFieldNode.js +++ b/examples/jsm/tsl/display/DepthOfFieldNode.js @@ -1,6 +1,13 @@ import { TempNode, NodeUpdateType } from 'three/webgpu'; import { convertToTexture, nodeObject, Fn, uv, uniform, vec2, vec4, clamp } from 'three/tsl'; +/** @module DepthOfFieldNode **/ + +/** + * Post processing node for creating depth of field (DOF) effect. + * + * @augments TempNode + */ class DepthOfFieldNode extends TempNode { static get type() { @@ -9,23 +16,78 @@ class DepthOfFieldNode extends TempNode { } + /** + * Constructs a new DOF node. + * + * @param {TextureNode} textureNode - The texture node that represents the input of the effect. + * @param {Node} viewZNode - Represents the viewZ depth values of the scene. + * @param {Node} focusNode - Defines the effect's focus which is the distance along the camera's look direction in world units. + * @param {Node} apertureNode - Defines the effect's aperture. + * @param {Node} maxblurNode - Defines the effect's maximum blur. + */ constructor( textureNode, viewZNode, focusNode, apertureNode, maxblurNode ) { super( 'vec4' ); + /** + * The texture node that represents the input of the effect. + * + * @type {TextureNode} + */ this.textureNode = textureNode; + + /** + * Represents the viewZ depth values of the scene. + * + * @type {Node} + */ this.viewZNode = viewZNode; + /** + * Defines the effect's focus which is the distance along the camera's look direction in world units. + * + * @type {Node} + */ this.focusNode = focusNode; + + /** + * Defines the effect's aperture. + * + * @type {Node} + */ this.apertureNode = apertureNode; + + /** + * Defines the effect's maximum blur. + * + * @type {Node} + */ this.maxblurNode = maxblurNode; + /** + * Represents the input's aspect ratio. + * + * @private + * @type {UniformNode} + */ this._aspect = uniform( 0 ); + /** + * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node updates + * its internal uniforms once per frame in `updateBefore()`. + * + * @type {String} + * @default 'frame' + */ this.updateBeforeType = NodeUpdateType.FRAME; } + /** + * This method is used to update the effect's uniforms once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ updateBefore() { const map = this.textureNode.value; @@ -34,6 +96,12 @@ class DepthOfFieldNode extends TempNode { } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {ShaderCallNodeInternal} + */ setup() { const textureNode = this.textureNode; @@ -116,4 +184,15 @@ class DepthOfFieldNode extends TempNode { export default DepthOfFieldNode; +/** + * TSL function for creating a depth-of-field effect (DOF) for post processing. + * + * @function + * @param {Node} node - The node that represents the input of the effect. + * @param {Node} viewZNode - Represents the viewZ depth values of the scene. + * @param {Node | Number} focus - Defines the effect's focus which is the distance along the camera's look direction in world units. + * @param {Node | Number} aperture - Defines the effect's aperture. + * @param {Node | Number} maxblur - Defines the effect's maximum blur. + * @returns {DepthOfFieldNode} + */ export const dof = ( node, viewZNode, focus = 1, aperture = 0.025, maxblur = 1 ) => nodeObject( new DepthOfFieldNode( convertToTexture( node ), nodeObject( viewZNode ), nodeObject( focus ), nodeObject( aperture ), nodeObject( maxblur ) ) ); diff --git a/examples/jsm/tsl/display/DotScreenNode.js b/examples/jsm/tsl/display/DotScreenNode.js index a8f15c225f71ef..a8fe8fa7814960 100644 --- a/examples/jsm/tsl/display/DotScreenNode.js +++ b/examples/jsm/tsl/display/DotScreenNode.js @@ -1,6 +1,13 @@ -import { Vector2, TempNode } from 'three/webgpu'; +import { TempNode } from 'three/webgpu'; import { nodeObject, Fn, uv, uniform, vec2, vec3, sin, cos, add, vec4, screenSize } from 'three/tsl'; +/** @module DotScreenNode **/ + +/** + * Post processing node for creating dot-screen effect. + * + * @augments TempNode + */ class DotScreenNode extends TempNode { static get type() { @@ -9,17 +16,46 @@ class DotScreenNode extends TempNode { } - constructor( inputNode, center = new Vector2( 0.5, 0.5 ), angle = 1.57, scale = 1 ) { + /** + * Constructs a new dot screen node. + * + * @param {Node} inputNode - The node that represents the input of the effect. + * @param {Number} [angle=1.57] - The rotation of the effect in radians. + * @param {Number} [scale=1] - The scale of the effect. A higher value means smaller dots. + */ + constructor( inputNode, angle = 1.57, scale = 1 ) { super( 'vec4' ); + /** + * The node that represents the input of the effect. + * + * @type {Node} + */ this.inputNode = inputNode; - this.center = uniform( center ); + + /** + * A uniform node that represents the rotation of the effect in radians. + * + * @type {UniformNode} + */ this.angle = uniform( angle ); + + /** + * A uniform node that represents the scale of the effect. A higher value means smaller dots. + * + * @type {UniformNode} + */ this.scale = uniform( scale ); } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {ShaderCallNodeInternal} + */ setup() { const inputNode = this.inputNode; @@ -29,7 +65,7 @@ class DotScreenNode extends TempNode { const s = sin( this.angle ); const c = cos( this.angle ); - const tex = uv().mul( screenSize ).sub( this.center ); + const tex = uv().mul( screenSize ); const point = vec2( c.mul( tex.x ).sub( s.mul( tex.y ) ), s.mul( tex.x ).add( c.mul( tex.y ) ) ).mul( this.scale ); return sin( point.x ).mul( sin( point.y ) ).mul( 4 ); @@ -56,4 +92,13 @@ class DotScreenNode extends TempNode { export default DotScreenNode; -export const dotScreen = ( node, center, angle, scale ) => nodeObject( new DotScreenNode( nodeObject( node ), center, angle, scale ) ); +/** + * TSL function for creating a dot-screen node for post processing. + * + * @function + * @param {Node} node - The node that represents the input of the effect. + * @param {Number} [angle=1.57] - The rotation of the effect in radians. + * @param {Number} [scale=1] - The scale of the effect. A higher value means smaller dots. + * @returns {DotScreenNode} + */ +export const dotScreen = ( node, angle, scale ) => nodeObject( new DotScreenNode( nodeObject( node ), angle, scale ) ); diff --git a/examples/jsm/tsl/display/FXAANode.js b/examples/jsm/tsl/display/FXAANode.js index bcc5df482422c9..cc502fb527ea28 100644 --- a/examples/jsm/tsl/display/FXAANode.js +++ b/examples/jsm/tsl/display/FXAANode.js @@ -1,6 +1,14 @@ import { Vector2, TempNode } from 'three/webgpu'; import { nodeObject, Fn, uniformArray, select, float, NodeUpdateType, uv, dot, clamp, uniform, convertToTexture, smoothstep, bool, vec2, vec3, If, Loop, max, min, Break, abs } from 'three/tsl'; +/** @module FXAANode **/ + +/** + * Post processing node for applying FXAA. This node requires sRGB input + * so tone mapping and color space conversion must happen before the anti-aliasing. + * + * @augments TempNode + */ class FXAANode extends TempNode { static get type() { @@ -9,19 +17,47 @@ class FXAANode extends TempNode { } + /** + * Constructs a new FXAA node. + * + * @param {TextureNode} textureNode - The texture node that represents the input of the effect. + */ constructor( textureNode ) { super( 'vec4' ); + /** + * The texture node that represents the input of the effect. + * + * @type {TextureNode} + */ this.textureNode = textureNode; + /** + * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node updates + * its internal uniforms once per frame in `updateBefore()`. + * + * @type {String} + * @default 'frame' + */ this.updateBeforeType = NodeUpdateType.FRAME; + /** + * A uniform node holding the inverse resolution value. + * + * @private + * @type {UniformNode} + */ this._invSize = uniform( new Vector2() ); } - updateBefore() { + /** + * This method is used to update the effect's uniforms once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ + updateBefore( /* frame */ ) { const map = this.textureNode.value; @@ -29,7 +65,13 @@ class FXAANode extends TempNode { } - setup() { + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {ShaderCallNodeInternal} + */ + setup( /* builder */ ) { const textureNode = this.textureNode.bias( - 100 ); const uvNode = textureNode.uvNode || uv(); @@ -313,4 +355,11 @@ class FXAANode extends TempNode { export default FXAANode; +/** + * TSL function for creating a FXAA node for anti-aliasing via post processing. + * + * @function + * @param {Node} node - The node that represents the input of the effect. + * @returns {FXAANode} + */ export const fxaa = ( node ) => nodeObject( new FXAANode( convertToTexture( node ) ) ); diff --git a/examples/jsm/tsl/display/FilmNode.js b/examples/jsm/tsl/display/FilmNode.js index 067ec6ab37903d..8d353c6b7cddf0 100644 --- a/examples/jsm/tsl/display/FilmNode.js +++ b/examples/jsm/tsl/display/FilmNode.js @@ -1,6 +1,13 @@ import { TempNode } from 'three/webgpu'; import { rand, Fn, fract, time, uv, clamp, mix, vec4, nodeProxy } from 'three/tsl'; +/** @module FilmNode **/ + +/** + * Post processing node for creating a film grain effect. + * + * @augments TempNode + */ class FilmNode extends TempNode { static get type() { @@ -9,17 +16,47 @@ class FilmNode extends TempNode { } + /** + * Constructs a new film node. + * + * @param {Node} inputNode - The node that represents the input of the effect. + * @param {Node?} [intensityNode=null] - A node that represents the effect's intensity. + * @param {Node?} [uvNode=null] - A node that allows to pass custom (e.g. animated) uv data. + */ constructor( inputNode, intensityNode = null, uvNode = null ) { super( 'vec4' ); + /** + * The node that represents the input of the effect. + * + * @type {Node} + */ this.inputNode = inputNode; + + /** + * A node that represents the effect's intensity. + * + * @type {Node} + */ this.intensityNode = intensityNode; + + /** + * A node that allows to pass custom (e.g. animated) uv data. + * + * @type {Node} + */ this.uvNode = uvNode; } - setup() { + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {ShaderCallNodeInternal} + */ + setup( /* builder */ ) { const uvNode = this.uvNode || uv(); @@ -50,4 +87,13 @@ class FilmNode extends TempNode { export default FilmNode; +/** + * TSL function for creating a film node for post processing. + * + * @function + * @param {Node} inputNode - The node that represents the input of the effect. + * @param {Node?} [intensityNode=null] - A node that represents the effect's intensity. + * @param {Node?} [uvNode=null] - A node that allows to pass custom (e.g. animated) uv data. + * @returns {FilmNode} + */ export const film = /*@__PURE__*/ nodeProxy( FilmNode ); diff --git a/examples/jsm/tsl/display/GaussianBlurNode.js b/examples/jsm/tsl/display/GaussianBlurNode.js index f5e80e5243c605..7bef4ef7e9b74f 100644 --- a/examples/jsm/tsl/display/GaussianBlurNode.js +++ b/examples/jsm/tsl/display/GaussianBlurNode.js @@ -1,8 +1,7 @@ import { RenderTarget, Vector2, NodeMaterial, PostProcessingUtils, QuadMesh, TempNode, NodeUpdateType } from 'three/webgpu'; import { nodeObject, Fn, If, float, uv, uniform, convertToTexture, vec2, vec4, passTexture, mul } from 'three/tsl'; -// WebGPU: The use of a single QuadMesh for both gaussian blur passes results in a single RenderObject with a SampledTexture binding that -// alternates between source textures and triggers creation of new BindGroups and BindGroupLayouts every frame. +/** @module GaussianBlurNode **/ const _quadMesh = /*@__PURE__*/ new QuadMesh(); @@ -34,6 +33,11 @@ const unpremult = /*@__PURE__*/ Fn( ( [ color ] ) => { ] } ); +/** + * Post processing node for creating a gaussian blur effect. + * + * @augments TempNode + */ class GaussianBlurNode extends TempNode { static get type() { @@ -42,33 +46,116 @@ class GaussianBlurNode extends TempNode { } + /** + * Constructs a new gaussian blur node. + * + * @param {TextureNode} textureNode - The texture node that represents the input of the effect. + * @param {Node} directionNode - Defines the direction and radius of the blur. + * @param {Number} sigma - Controls the kernel of the blur filter. Higher values mean a wider blur radius. + */ constructor( textureNode, directionNode = null, sigma = 2 ) { super( 'vec4' ); + /** + * The texture node that represents the input of the effect. + * + * @type {TextureNode} + */ this.textureNode = textureNode; + + /** + * Defines the direction and radius of the blur. + * + * @type {Node} + */ this.directionNode = directionNode; + + /** + * Controls the kernel of the blur filter. Higher values mean a wider blur radius. + * + * @type {Number} + */ this.sigma = sigma; + /** + * A uniform node holding the inverse resolution value. + * + * @private + * @type {UniformNode} + */ this._invSize = uniform( new Vector2() ); + + /** + * Gaussian blur is applied in two passes (horizontal, vertical). + * This node controls the direction of each pass. + * + * @private + * @type {UniformNode} + */ this._passDirection = uniform( new Vector2() ); + /** + * The render target used for the horizontal pass. + * + * @private + * @type {RenderTarget} + */ this._horizontalRT = new RenderTarget( 1, 1, { depthBuffer: false } ); this._horizontalRT.texture.name = 'GaussianBlurNode.horizontal'; + + /** + * The render target used for the vertical pass. + * + * @private + * @type {RenderTarget} + */ this._verticalRT = new RenderTarget( 1, 1, { depthBuffer: false } ); this._verticalRT.texture.name = 'GaussianBlurNode.vertical'; + /** + * The result of the effect is represented as a separate texture node. + * + * @private + * @type {PassTextureNode} + */ this._textureNode = passTexture( this, this._verticalRT.texture ); this._textureNode.uvNode = textureNode.uvNode; + /** + * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders + * its effect once per frame in `updateBefore()`. + * + * @type {String} + * @default 'frame' + */ this.updateBeforeType = NodeUpdateType.FRAME; + /** + * Controls the resolution of the effect. + * + * @type {Vector2} + * @default (1,1) + */ this.resolution = new Vector2( 1, 1 ); + /** + * Whether the effect should use premultiplied alpha or not. Set this to `true` + * if you are going to blur texture input with transparency. + * + * @type {Boolean} + * @default false + */ this.premultipliedAlpha = false; } + /** + * Sets the given premultiplied alpha value. + * + * @param {Boolean} value - Whether the effect should use premultiplied alpha or not. + * @return {GaussianBlurNode} height - A reference to this node. + */ setPremultipliedAlpha( value ) { this.premultipliedAlpha = value; @@ -77,12 +164,23 @@ class GaussianBlurNode extends TempNode { } + /** + * Returns the premultiplied alpha value. + * + * @return {Boolean} Whether the effect should use premultiplied alpha or not. + */ getPremultipliedAlpha() { return this.premultipliedAlpha; } + /** + * Sets the size of the effect. + * + * @param {Number} width - The width of the effect. + * @param {Number} height - The height of the effect. + */ setSize( width, height ) { width = Math.max( Math.round( width * this.resolution.x ), 1 ); @@ -94,6 +192,11 @@ class GaussianBlurNode extends TempNode { } + /** + * This method is used to render the effect once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ updateBefore( frame ) { const { renderer } = frame; @@ -141,12 +244,23 @@ class GaussianBlurNode extends TempNode { } + /** + * Returns the result of the effect as a texture node. + * + * @return {PassTextureNode} A texture node that represents the result of the effect. + */ getTextureNode() { return this._textureNode; } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {PassTextureNode} + */ setup( builder ) { const textureNode = this.textureNode; @@ -220,6 +334,10 @@ class GaussianBlurNode extends TempNode { } + /** + * Frees internal resources. This method should be called + * when the effect is no longer required. + */ dispose() { this._horizontalRT.dispose(); @@ -227,6 +345,13 @@ class GaussianBlurNode extends TempNode { } + /** + * Computes gaussian coefficients depending on the given kernel radius. + * + * @private + * @param {Number} kernelRadius - The kernel radius. + * @return {Array} + */ _getCoefficients( kernelRadius ) { const coefficients = []; @@ -245,5 +370,24 @@ class GaussianBlurNode extends TempNode { export default GaussianBlurNode; +/** + * TSL function for creating gaussian blur node for post processing. + * + * @function + * @param {Node} node - The node that represents the input of the effect. + * @param {Node} directionNode - Defines the direction and radius of the blur. + * @param {Number} sigma - Controls the kernel of the blur filter. Higher values mean a wider blur radius. + * @returns {GaussianBlurNode} + */ export const gaussianBlur = ( node, directionNode, sigma ) => nodeObject( new GaussianBlurNode( convertToTexture( node ), directionNode, sigma ) ); + +/** + * TSL function for creating gaussian blur node for post processing with enabled premultiplied alpha. + * + * @function + * @param {Node} node - The node that represents the input of the effect. + * @param {Node} directionNode - Defines the direction and radius of the blur. + * @param {Number} sigma - Controls the kernel of the blur filter. Higher values mean a wider blur radius. + * @returns {GaussianBlurNode} + */ export const premultipliedGaussianBlur = ( node, directionNode, sigma ) => nodeObject( new GaussianBlurNode( convertToTexture( node ), directionNode, sigma ).setPremultipliedAlpha( true ) ); diff --git a/examples/jsm/tsl/display/Lut3DNode.js b/examples/jsm/tsl/display/Lut3DNode.js index 49687b76531d72..1c3fac61ffe227 100644 --- a/examples/jsm/tsl/display/Lut3DNode.js +++ b/examples/jsm/tsl/display/Lut3DNode.js @@ -1,6 +1,13 @@ import { TempNode } from 'three/webgpu'; import { nodeObject, Fn, float, uniform, vec3, vec4, mix } from 'three/tsl'; +/** @module Lut3DNode **/ + +/** + * A post processing node for color grading via lookup tables. + * + * @augments TempNode + */ class Lut3DNode extends TempNode { static get type() { @@ -9,17 +16,54 @@ class Lut3DNode extends TempNode { } + /** + * Constructs a new LUT node. + * + * @param {Node} inputNode - The node that represents the input of the effect. + * @param {TextureNode} lutNode - A texture node that represents the lookup table. + * @param {Number} size - The size of the lookup table. + * @param {Node} intensityNode - Controls the intensity of the effect. + */ constructor( inputNode, lutNode, size, intensityNode ) { super( 'vec4' ); + /** + * The node that represents the input of the effect. + * + * @type {Node} + */ this.inputNode = inputNode; + + /** + * A texture node that represents the lookup table. + * + * @type {TextureNode} + */ this.lutNode = lutNode; + + /** + * The size of the lookup table. + * + * @type {UniformNode} + */ this.size = uniform( size ); + + /** + * Controls the intensity of the effect. + * + * @type {Node} + */ this.intensityNode = intensityNode; } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {ShaderCallNodeInternal} + */ setup() { const { inputNode, lutNode } = this; @@ -52,4 +96,14 @@ class Lut3DNode extends TempNode { export default Lut3DNode; +/** + * TSL function for creating a LUT node for color grading via post processing. + * + * @function + * @param {Node} node - The node that represents the input of the effect. + * @param {TextureNode} lut - A texture node that represents the lookup table. + * @param {Number} size - The size of the lookup table. + * @param {Node | Number} intensity - Controls the intensity of the effect. + * @returns {Lut3DNode} + */ export const lut3D = ( node, lut, size, intensity ) => nodeObject( new Lut3DNode( nodeObject( node ), nodeObject( lut ), size, nodeObject( intensity ) ) ); diff --git a/examples/jsm/tsl/display/MotionBlur.js b/examples/jsm/tsl/display/MotionBlur.js index 20402df5173dde..85d72903daf7a2 100644 --- a/examples/jsm/tsl/display/MotionBlur.js +++ b/examples/jsm/tsl/display/MotionBlur.js @@ -1,6 +1,16 @@ - import { Fn, float, uv, Loop, int } from 'three/tsl'; +/** @module MotionBlur **/ + +/** + * Applies a motion blur effect to the given input node. + * + * @function + * @param {Node} inputNode - The input node to apply the motion blur for. + * @param {Node} velocity - The motion vectors of the beauty pass. + * @param {Node} [numSamples=int(16)] - How many samples the effect should use. A higher value results in better quality but is also more expensive. + * @return {Node} The input node with the motion blur effect applied. + */ export const motionBlur = /*@__PURE__*/ Fn( ( [ inputNode, velocity, numSamples = int( 16 ) ] ) => { const sampleColor = ( uv ) => inputNode.sample( uv ); diff --git a/examples/jsm/tsl/display/ParallaxBarrierPassNode.js b/examples/jsm/tsl/display/ParallaxBarrierPassNode.js index 43da8b02cfd7df..774b4420df9111 100644 --- a/examples/jsm/tsl/display/ParallaxBarrierPassNode.js +++ b/examples/jsm/tsl/display/ParallaxBarrierPassNode.js @@ -2,6 +2,13 @@ import { NodeMaterial } from 'three/webgpu'; import { nodeObject, Fn, vec4, uv, If, mod, screenCoordinate } from 'three/tsl'; import StereoCompositePassNode from './StereoCompositePassNode.js'; +/** @module ParallaxBarrierPassNode **/ + +/** + * A render pass node that creates a parallax barrier effect. + * + * @augments StereoCompositePassNode + */ class ParallaxBarrierPassNode extends StereoCompositePassNode { static get type() { @@ -10,14 +17,33 @@ class ParallaxBarrierPassNode extends StereoCompositePassNode { } + /** + * Constructs a new parallax barrier pass node. + * + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + */ constructor( scene, camera ) { super( scene, camera ); + /** + * This flag can be used for type testing. + * + * @type {Boolean} + * @readonly + * @default true + */ this.isParallaxBarrierPassNode = true; } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {PassTextureNode} + */ setup( builder ) { const uvNode = uv(); @@ -52,4 +78,12 @@ class ParallaxBarrierPassNode extends StereoCompositePassNode { export default ParallaxBarrierPassNode; +/** + * TSL function for creating an parallax barrier pass node. + * + * @function + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + * @returns {ParallaxBarrierPassNode} + */ export const parallaxBarrierPass = ( scene, camera ) => nodeObject( new ParallaxBarrierPassNode( scene, camera ) ); diff --git a/examples/jsm/tsl/display/PixelationPassNode.js b/examples/jsm/tsl/display/PixelationPassNode.js index 59b5aa53623ebd..77258e30553bea 100644 --- a/examples/jsm/tsl/display/PixelationPassNode.js +++ b/examples/jsm/tsl/display/PixelationPassNode.js @@ -1,6 +1,14 @@ import { NearestFilter, Vector4, TempNode, NodeUpdateType, PassNode } from 'three/webgpu'; import { nodeObject, Fn, float, uv, uniform, convertToTexture, vec2, vec3, clamp, floor, dot, smoothstep, If, sign, step, mrt, output, normalView, property } from 'three/tsl'; +/** @module PixelationPassNode **/ + +/** + * A inner node definition that implements the actual pixelation TSL code. + * + * @inner + * @augments TempNode + */ class PixelationNode extends TempNode { static get type() { @@ -9,30 +17,85 @@ class PixelationNode extends TempNode { } + /** + * Constructs a new pixelation node. + * + * @param {TextureNode} textureNode - The texture node that represents the beauty pass. + * @param {TextureNode} depthNode - The texture that represents the beauty's depth. + * @param {TextureNode} normalNode - The texture that represents the beauty's normals. + * @param {Node} pixelSize - The pixel size. + * @param {Node} normalEdgeStrength - The normal edge strength. + * @param {Node} depthEdgeStrength - The depth edge strength. + */ constructor( textureNode, depthNode, normalNode, pixelSize, normalEdgeStrength, depthEdgeStrength ) { super( 'vec4' ); - // Input textures - + /** + * The texture node that represents the beauty pass. + * + * @type {TextureNode} + */ this.textureNode = textureNode; + + /** + * The texture that represents the beauty's depth. + * + * @type {TextureNode} + */ this.depthNode = depthNode; - this.normalNode = normalNode; - // Input uniforms + /** + * The texture that represents the beauty's normals. + * + * @type {TextureNode} + */ + this.normalNode = normalNode; + /** + * The pixel size. + * + * @type {Node} + */ this.pixelSize = pixelSize; + + /** + * The pixel size. + * + * @type {Node} + */ this.normalEdgeStrength = normalEdgeStrength; - this.depthEdgeStrength = depthEdgeStrength; - // Private uniforms + /** + * The depth edge strength. + * + * @type {Node} + */ + this.depthEdgeStrength = depthEdgeStrength; + /** + * Uniform node that represents the resolution. + * + * @type {Node} + */ this._resolution = uniform( new Vector4() ); + /** + * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node updates + * its internal uniforms once per frame in `updateBefore()`. + * + * @type {String} + * @default 'frame' + */ this.updateBeforeType = NodeUpdateType.FRAME; } + /** + * This method is used to update uniforms once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ updateBefore() { const map = this.textureNode.value; @@ -44,6 +107,12 @@ class PixelationNode extends TempNode { } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {ShaderCallNodeInternal} + */ setup() { const { textureNode, depthNode, normalNode } = this; @@ -148,6 +217,11 @@ class PixelationNode extends TempNode { const pixelation = ( node, depthNode, normalNode, pixelSize = 6, normalEdgeStrength = 0.3, depthEdgeStrength = 0.4 ) => nodeObject( new PixelationNode( convertToTexture( node ), convertToTexture( depthNode ), convertToTexture( normalNode ), nodeObject( pixelSize ), nodeObject( normalEdgeStrength ), nodeObject( depthEdgeStrength ) ) ); +/** + * A special render pass node that renders the scene with a pixelation effect. + * + * @augments PassNode + */ class PixelationPassNode extends PassNode { static get type() { @@ -156,14 +230,50 @@ class PixelationPassNode extends PassNode { } + /** + * Constructs a new pixelation pass node. + * + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + * @param {Node | Number} [pixelSize=6] - The pixel size. + * @param {Node | Number} [normalEdgeStrength=03] - The normal edge strength. + * @param {Node | Number} [depthEdgeStrength=03] - The depth edge strength. + */ constructor( scene, camera, pixelSize = 6, normalEdgeStrength = 0.3, depthEdgeStrength = 0.4 ) { super( PassNode.COLOR, scene, camera, { minFilter: NearestFilter, magFilter: NearestFilter } ); + /** + * The pixel size. + * + * @type {Number} + * @default 6 + */ this.pixelSize = pixelSize; + + /** + * The normal edge strength. + * + * @type {Number} + * @default 0.3 + */ this.normalEdgeStrength = normalEdgeStrength; + + /** + * The depth edge strength. + * + * @type {Number} + * @default 0.4 + */ this.depthEdgeStrength = depthEdgeStrength; + /** + * This flag can be used for type testing. + * + * @type {Boolean} + * @readonly + * @default true + */ this.isPixelationPassNode = true; this._mrt = mrt( { @@ -173,6 +283,12 @@ class PixelationPassNode extends PassNode { } + /** + * Sets the size of the pass. + * + * @param {Number} width - The width of the pass. + * @param {Number} height - The height of the pass. + */ setSize( width, height ) { const pixelSize = this.pixelSize.value ? this.pixelSize.value : this.pixelSize; @@ -184,6 +300,12 @@ class PixelationPassNode extends PassNode { } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {PixelationNode} + */ setup() { const color = super.getTextureNode( 'output' ); @@ -196,6 +318,17 @@ class PixelationPassNode extends PassNode { } +/** + * TSL function for creating a pixelation render pass node for post processing. + * + * @function + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + * @param {Node | Number} [pixelSize=6] - The pixel size. + * @param {Node | Number} [normalEdgeStrength=03] - The normal edge strength. + * @param {Node | Number} [depthEdgeStrength=03] - The depth edge strength. + * @returns {PixelationPassNode} + */ export const pixelationPass = ( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength ) => nodeObject( new PixelationPassNode( scene, camera, pixelSize, normalEdgeStrength, depthEdgeStrength ) ); export default PixelationPassNode; diff --git a/examples/jsm/tsl/display/RGBShiftNode.js b/examples/jsm/tsl/display/RGBShiftNode.js index 1f311dee94931a..1548ab697d20da 100644 --- a/examples/jsm/tsl/display/RGBShiftNode.js +++ b/examples/jsm/tsl/display/RGBShiftNode.js @@ -1,6 +1,14 @@ import { TempNode } from 'three/webgpu'; import { nodeObject, Fn, uv, uniform, vec2, sin, cos, vec4, convertToTexture } from 'three/tsl'; +/** @module RGBShiftNode **/ + +/** + * Post processing node for shifting/splitting RGB color channels. The effect + * separates color channels and offsets them from each other. + * + * @augments TempNode + */ class RGBShiftNode extends TempNode { static get type() { @@ -9,17 +17,47 @@ class RGBShiftNode extends TempNode { } + /** + * Constructs a new RGB shift node. + * + * @param {TextureNode} textureNode - The texture node that represents the input of the effect. + * @param {Number} [amount=0.005] - The amount of the RGB shift. + * @param {Number} [angle=0] - Defines the orientation in which colors are shifted. + */ constructor( textureNode, amount = 0.005, angle = 0 ) { super( 'vec4' ); + /** + * The texture node that represents the input of the effect. + * + * @type {TextureNode} + */ this.textureNode = textureNode; + + /** + * The amount of the RGB shift. + * + * @type {UniformNode} + */ this.amount = uniform( amount ); + + /** + * Defines in which direction colors are shifted. + * + * @type {UniformNode} + */ this.angle = uniform( angle ); } - setup() { + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {ShaderCallNodeInternal} + */ + setup( /* builder */ ) { const { textureNode } = this; @@ -46,4 +84,13 @@ class RGBShiftNode extends TempNode { export default RGBShiftNode; +/** + * TSL function for creating a RGB shift or split effect for post processing. + * + * @function + * @param {Node} node - The node that represents the input of the effect. + * @param {Number} [amount=0.005] - The amount of the RGB shift. + * @param {Number} [angle=0] - Defines in which direction colors are shifted. + * @returns {RGBShiftNode} + */ export const rgbShift = ( node, amount, angle ) => nodeObject( new RGBShiftNode( convertToTexture( node ), amount, angle ) ); diff --git a/examples/jsm/tsl/display/SMAANode.js b/examples/jsm/tsl/display/SMAANode.js index 0e7bf75c6c424a..6ea22c985f1e55 100644 --- a/examples/jsm/tsl/display/SMAANode.js +++ b/examples/jsm/tsl/display/SMAANode.js @@ -1,17 +1,23 @@ import { HalfFloatType, LinearFilter, NearestFilter, RenderTarget, Texture, Vector2, QuadMesh, NodeMaterial, TempNode, PostProcessingUtils } from 'three/webgpu'; import { abs, nodeObject, Fn, NodeUpdateType, uv, uniform, convertToTexture, varyingProperty, vec2, vec4, modelViewProjection, passTexture, max, step, dot, float, texture, If, Loop, int, Break, sqrt, sign, mix } from 'three/tsl'; +/** @module SMAANode **/ + const _quadMesh = /*@__PURE__*/ new QuadMesh(); const _size = /*@__PURE__*/ new Vector2(); let _rendererState; /** - * Port of Subpixel Morphological Antialiasing (SMAA) v2.8 - * Preset: SMAA 1x Medium (with color edge detection) - * https://github.com/iryoku/smaa/releases/tag/v2.8 + * Post processing node for applying SMAA. Unlike FXAA, this node + * should be applied before converting colors to sRGB. SMAA should produce + * better results than FXAA but is also more expensive to execute. + * + * Used Preset: SMAA 1x Medium (with color edge detection) + * Reference: {@link https://github.com/iryoku/smaa/releases/tag/v2.8}. + * + * @augments TempNode */ - class SMAANode extends TempNode { static get type() { @@ -20,22 +26,55 @@ class SMAANode extends TempNode { } + /** + * Constructs a new SMAA node. + * + * @param {TextureNode} textureNode - The texture node that represents the input of the effect. + */ constructor( textureNode ) { super( 'vec4' ); + /** + * The texture node that represents the input of the effect. + * + * @type {TextureNode} + */ this.textureNode = textureNode; + /** + * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders + * its effect once per frame in `updateBefore()`. + * + * @type {String} + * @default 'frame' + */ this.updateBeforeType = NodeUpdateType.FRAME; - // render targets - + /** + * The render target used for the edges pass. + * + * @private + * @type {RenderTarget} + */ this._renderTargetEdges = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } ); this._renderTargetEdges.texture.name = 'SMAANode.edges'; + /** + * The render target used for the weights pass. + * + * @private + * @type {RenderTarget} + */ this._renderTargetWeights = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } ); this._renderTargetWeights.texture.name = 'SMAANode.weights'; + /** + * The render target used for the blend pass. + * + * @private + * @type {RenderTarget} + */ this._renderTargetBlend = new RenderTarget( 1, 1, { depthBuffer: false, type: HalfFloatType } ); this._renderTargetBlend.texture.name = 'SMAANode.blend'; @@ -44,7 +83,7 @@ class SMAANode extends TempNode { const scope = this; const areaTextureImage = new Image(); - areaTextureImage.src = this.getAreaTexture(); + areaTextureImage.src = this._getAreaTexture(); areaTextureImage.onload = function () { // assigning data to HTMLImageElement.src is asynchronous (see #15162) @@ -52,6 +91,12 @@ class SMAANode extends TempNode { }; + /** + * Represents the "area" texture used by the SMAA implementation. + * + * @private + * @type {RenderTarget} + */ this._areaTexture = new Texture(); this._areaTexture.name = 'SMAANode.area'; this._areaTexture.image = areaTextureImage; @@ -60,7 +105,7 @@ class SMAANode extends TempNode { this._areaTexture.flipY = false; const searchTextureImage = new Image(); - searchTextureImage.src = this.getSearchTexture(); + searchTextureImage.src = this._getSearchTexture(); searchTextureImage.onload = function () { // assigning data to HTMLImageElement.src is asynchronous (see #15162) @@ -68,6 +113,12 @@ class SMAANode extends TempNode { }; + /** + * Represents the "search" texture used by the SMAA implementation. + * + * @private + * @type {RenderTarget} + */ this._searchTexture = new Texture(); this._searchTexture.name = 'SMAANode.search'; this._searchTexture.image = searchTextureImage; @@ -76,37 +127,100 @@ class SMAANode extends TempNode { this._searchTexture.generateMipmaps = false; this._searchTexture.flipY = false; - // uniforms - + /** + * A uniform node holding the inverse resolution value. + * + * @private + * @type {UniformNode} + */ this._invSize = uniform( new Vector2() ); + + /** + * A uniform texture node holding the area texture. + * + * @private + * @type {TextureNode} + */ this._areaTextureUniform = texture( this._areaTexture ); + + /** + * A uniform texture node holding the search texture. + * + * @private + * @type {TextureNode} + */ this._searchTextureUniform = texture( this._searchTexture ); + + /** + * A uniform texture node representing the edges pass. + * + * @private + * @type {TextureNode} + */ this._edgesTextureUniform = texture( this._renderTargetEdges.texture ); - this._weightsTextureUniform = texture( this._renderTargetWeights.texture ); - // materials + /** + * A uniform texture node representing the weights pass. + * + * @private + * @type {TextureNode} + */ + this._weightsTextureUniform = texture( this._renderTargetWeights.texture ); + /** + * The node material that holds the TSL for rendering the edges pass. + * + * @private + * @type {NodeMaterial} + */ this._materialEdges = new NodeMaterial(); this._materialEdges.name = 'SMAANode.edges'; + /** + * The node material that holds the TSL for rendering the weights pass. + * + * @private + * @type {NodeMaterial} + */ this._materialWeights = new NodeMaterial(); this._materialWeights.name = 'SMAANode.weights'; + /** + * The node material that holds the TSL for rendering the blend pass. + * + * @private + * @type {NodeMaterial} + */ this._materialBlend = new NodeMaterial(); this._materialBlend.name = 'SMAANode.blend'; - // - + /** + * The result of the effect is represented as a separate texture node. + * + * @private + * @type {PassTextureNode} + */ this._textureNode = passTexture( this, this._renderTargetBlend.texture ); } + /** + * Returns the result of the effect as a texture node. + * + * @return {PassTextureNode} A texture node that represents the result of the effect. + */ getTextureNode() { return this._textureNode; } + /** + * Sets the size of the effect. + * + * @param {Number} width - The width of the effect. + * @param {Number} height - The height of the effect. + */ setSize( width, height ) { this._invSize.value.set( 1 / width, 1 / height ); @@ -117,6 +231,11 @@ class SMAANode extends TempNode { } + /** + * This method is used to render the effect once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ updateBefore( frame ) { const { renderer } = frame; @@ -155,6 +274,12 @@ class SMAANode extends TempNode { } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {PassTextureNode} + */ setup( builder ) { const SMAA_THRESHOLD = 0.1; @@ -586,6 +711,10 @@ class SMAANode extends TempNode { } + /** + * Frees internal resources. This method should be called + * when the effect is no longer required. + */ dispose() { this._renderTargetEdges.dispose(); @@ -601,13 +730,25 @@ class SMAANode extends TempNode { } - getAreaTexture() { + /** + * Returns the area texture as a Base64 string. + * + * @private + * @return {String} The area texture. + */ + _getAreaTexture() { return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKAAAAIwCAIAAACOVPcQAACBeklEQVR42u39W4xlWXrnh/3WWvuciIzMrKxrV8/0rWbY0+SQFKcb4owIkSIFCjY9AC1BT/LYBozRi+EX+cV+8IMsYAaCwRcBwjzMiw2jAWtgwC8WR5Q8mDFHZLNHTarZGrLJJllt1W2qKrsumZWZcTvn7L3W54e1vrXX3vuciLPPORFR1XE2EomorB0nVuz//r71re/y/1eMvb4Cb3N11xV/PP/2v4UBAwJG/7H8urx6/25/Gf8O5hypMQ0EEEQwAqLfoN/Z+97f/SW+/NvcgQk4sGBJK6H7N4PFVL+K+e0N11yNfkKvwUdwdlUAXPHHL38oa15f/i/46Ih6SuMSPmLAYAwyRKn7dfMGH97jaMFBYCJUgotIC2YAdu+LyW9vvubxAP8kAL8H/koAuOKP3+q6+xGnd5kdYCeECnGIJViwGJMAkQKfDvB3WZxjLKGh8VSCCzhwEWBpMc5/kBbjawT4HnwJfhr+pPBIu7uu+OOTo9vsmtQcniMBGkKFd4jDWMSCRUpLjJYNJkM+IRzQ+PQvIeAMTrBS2LEiaiR9b/5PuT6Ap/AcfAFO4Y3dA3DFH7/VS+M8k4baEAQfMI4QfbVDDGIRg7GKaIY52qAjTAgTvGBAPGIIghOCYAUrGFNgzA7Q3QhgCwfwAnwe5vDejgG44o/fbm1C5ZlYQvQDARPAIQGxCWBM+wWl37ZQESb4gImexGMDouhGLx1Cst0Saa4b4AqO4Hk4gxo+3DHAV/nx27p3JziPM2pVgoiia5MdEzCGULprIN7gEEeQ5IQxEBBBQnxhsDb5auGmAAYcHMA9eAAz8PBol8/xij9+C4Djlim4gJjWcwZBhCBgMIIYxGAVIkH3ZtcBuLdtRFMWsPGoY9rN+HoBji9VBYdwD2ZQg4cnO7OSq/z4rU5KKdwVbFAjNojCQzTlCLPFSxtamwh2jMUcEgg2Wm/6XgErIBhBckQtGN3CzbVacERgCnfgLswhnvqf7QyAq/z4rRZm1YglYE3affGITaZsdIe2FmMIpnOCap25I6jt2kCwCW0D1uAD9sZctNGXcQIHCkINDQgc78aCr+zjtw3BU/ijdpw3zhCwcaONwBvdeS2YZKkJNJsMPf2JKEvC28RXxxI0ASJyzQCjCEQrO4Q7sFArEzjZhaFc4cdv+/JFdKULM4px0DfUBI2hIsy06BqLhGTQEVdbfAIZXYMPesq6VoCHICzUyjwInO4Y411//LYLs6TDa9wvg2CC2rElgAnpTBziThxaL22MYhzfkghz6GAs2VHbbdM91VZu1MEEpupMMwKyVTb5ij9+u4VJG/5EgEMMmFF01cFai3isRbKbzb+YaU/MQbAm2XSMoUPAmvZzbuKYRIFApbtlrfFuUGd6vq2hXNnH78ZLh/iFhsQG3T4D1ib7k5CC6vY0DCbtrohgLEIClXiGtl10zc0CnEGIhhatLBva7NP58Tvw0qE8yWhARLQ8h4+AhQSP+I4F5xoU+VilGRJs6wnS7ruti/4KvAY/CfdgqjsMy4pf8fodQO8/gnuX3f/3xi3om1/h7THr+co3x93PP9+FBUfbNUjcjEmhcrkT+8K7ml7V10Jo05mpIEFy1NmCJWx9SIKKt+EjAL4Ez8EBVOB6havuT/rByPvHXK+9zUcfcbb254+9fydJknYnRr1oGfdaiAgpxu1Rx/Rek8KISftx3L+DfsLWAANn8Hvw0/AFeAGO9DFV3c6D+CcWbL8Dj9e7f+T1k8AZv/d7+PXWM/Z+VvdCrIvuAKO09RpEEQJM0Ci6+B4xhTWr4cZNOvhktabw0ta0rSJmqz3Yw5/AKXwenod7cAhTmBSPKf6JBdvH8IP17h95pXqw50/+BFnj88fev4NchyaK47OPhhtI8RFSvAfDSNh0Ck0p2gLxGkib5NJj/JWCr90EWQJvwBzO4AHcgztwAFN1evHPUVGwfXON+0debT1YeGON9Yy9/63X+OguiwmhIhQhD7l4sMqlG3D86Suc3qWZ4rWjI1X7u0Ytw6x3rIMeIOPDprfe2XzNgyj6PahhBjO4C3e6puDgXrdg+/5l948vF3bqwZetZ+z9Rx9zdIY5pInPK4Nk0t+l52xdK2B45Qd87nM8fsD5EfUhIcJcERw4RdqqH7Yde5V7m1vhNmtedkz6EDzUMF/2jJYWbC+4fzzA/Y+/8PPH3j9dcBAPIRP8JLXd5BpAu03aziOL3VVHZzz3CXWDPWd+SH2AnxIqQoTZpo9Ckc6HIrFbAbzNmlcg8Ag8NFDDAhbJvTBZXbC94P7t68EXfv6o+21gUtPETU7bbkLxvNKRFG2+KXzvtObonPP4rBvsgmaKj404DlshFole1Glfh02fE7bYR7dZ82oTewIBGn1Md6CG6YUF26X376oevOLzx95vhUmgblI6LBZwTCDY7vMq0op5WVXgsObOXJ+1x3qaBl9j1FeLxbhU9w1F+Wiba6s1X/TBz1LnUfuYDi4r2C69f1f14BWfP+p+W2GFKuC9phcELMYRRLur9DEZTUdEH+iEqWdaM7X4WOoPGI+ZYD2+wcQ+y+ioHUZ9dTDbArzxmi/bJI9BND0Ynd6lBdve/butBw8+f/T9D3ABa3AG8W3VPX4hBin+bj8dMMmSpp5pg7fJ6xrBFE2WQQEWnV8Qg3FbAWzYfM1rREEnmvkN2o1+acG2d/9u68GDzx91v3mAjb1zkpqT21OipPKO0b9TO5W0nTdOmAQm0TObts3aBKgwARtoPDiCT0gHgwnbArzxmtcLc08HgF1asN0C4Ms/fvD5I+7PhfqyXE/b7RbbrGyRQRT9ARZcwAUmgdoz0ehJ9Fn7QAhUjhDAQSw0bV3T3WbNa59jzmiP6GsWbGXDX2ytjy8+f9T97fiBPq9YeLdBmyuizZHaqXITnXiMUEEVcJ7K4j3BFPurtB4bixW8wTpweL8DC95szWMOqucFYGsWbGU7p3TxxxefP+r+oTVktxY0v5hbq3KiOKYnY8ddJVSBxuMMVffNbxwIOERShst73HZ78DZrHpmJmH3K6sGz0fe3UUj0eyRrSCGTTc+rjVNoGzNSv05srAxUBh8IhqChiQgVNIIBH3AVPnrsnXQZbLTm8ammv8eVXn/vWpaTem5IXRlt+U/LA21zhSb9cye6jcOfCnOwhIAYXAMVTUNV0QhVha9xjgA27ODJbLbmitt3tRN80lqG6N/khgot4ZVlOyO4WNg3OIMzhIZQpUEHieg2im6F91hB3I2tubql6BYNN9Hj5S7G0G2tahslBWKDnOiIvuAEDzakDQKDNFQT6gbn8E2y4BBubM230YIpBnDbMa+y3dx0n1S0BtuG62lCCXwcY0F72T1VRR3t2ONcsmDjbmzNt9RFs2LO2hQNyb022JisaI8rAWuw4HI3FuAIhZdOGIcdjLJvvObqlpqvWTJnnQbyi/1M9O8UxWhBs//H42I0q1Yb/XPGONzcmm+ri172mHKvZBpHkJaNJz6v9jxqiklDj3U4CA2ugpAaYMWqNXsdXbmJNd9egCnJEsphXNM+MnK3m0FCJ5S1kmJpa3DgPVbnQnPGWIDspW9ozbcO4K/9LkfaQO2KHuqlfFXSbdNzcEcwoqNEFE9zcIXu9/6n/ym/BC/C3aJLzEKPuYVlbFnfhZ8kcWxV3dbv4bKl28566wD+8C53aw49lTABp9PWbsB+knfc/Li3eVizf5vv/xmvnPKg5ihwKEwlrcHqucuVcVOxEv8aH37E3ZqpZypUulrHEtIWKUr+txHg+ojZDGlwnqmkGlzcVi1dLiNSJiHjfbRNOPwKpx9TVdTn3K05DBx4psIk4Ei8aCkJahRgffk4YnEXe07T4H2RR1u27E6wfQsBDofUgjFUFnwC2AiVtA+05J2zpiDK2Oa0c5fmAecN1iJzmpqFZxqYBCYhFTCsUNEmUnIcZ6aEA5rQVhEywG6w7HSW02XfOoBlQmjwulOFQAg66SvJblrTEX1YtJ3uG15T/BH1OfOQeuR8g/c0gdpT5fx2SKbs9EfHTKdM8A1GaJRHLVIwhcGyydZsbifAFVKl5EMKNU2Hryo+06BeTgqnxzYjThVySDikbtJPieco75lYfKAJOMEZBTjoITuWHXXZVhcUDIS2hpiXHV9Ku4u44bN5OYLDOkJo8w+xJSMbhBRHEdEs9JZUCkQrPMAvaHyLkxgkEHxiNkx/x2YB0mGsQ8EUWj/stW5YLhtS5SMu+/YBbNPDCkGTUybN8krRLBGPlZkVOA0j+a1+rkyQKWGaPHPLZOkJhioQYnVZ2hS3zVxMtgC46KuRwbJNd9nV2PHgb36F194ecf/Yeu2vAFe5nm/bRBFrnY4BauE8ERmZRFUn0k8hbftiVYSKMEme2dJCJSCGYAlNqh87bXOPdUkGy24P6d1ll21MBqqx48Fvv8ZHH8HZFY7j/uAq1xMJUFqCSUlJPmNbIiNsmwuMs/q9CMtsZsFO6SprzCS1Z7QL8xCQClEelpjTduDMsmWD8S1PT152BtvmIGvUeDA/yRn83u/x0/4qxoPHjx+PXY9pqX9bgMvh/Nz9kpP4pOe1/fYf3axUiMdHLlPpZCNjgtNFAhcHEDxTumNONhHrBduW+vOyY++70WWnPXj98eA4kOt/mj/5E05l9+O4o8ePx67HFqyC+qSSnyselqjZGaVK2TadbFLPWAQ4NBhHqDCCV7OTpo34AlSSylPtIdd2AJZlyzYQrDJ5lcWGNceD80CunPLGGzsfD+7wRb95NevJI5docQ3tgCyr5bGnyaPRlmwNsFELViOOx9loebGNq2moDOKpHLVP5al2cymWHbkfzGXL7kfRl44H9wZy33tvt+PB/Xnf93e+nh5ZlU18wCiRUa9m7kib9LYuOk+hudQNbxwm0AQqbfloimaB2lM5fChex+ylMwuTbfmXQtmWlenZljbdXTLuOxjI/fDDHY4Hjx8/Hrse0zXfPFxbUN1kKqSCCSk50m0Ajtx3ub9XHBKHXESb8iO6E+qGytF4nO0OG3SXzbJlhxBnKtKyl0NwybjvYCD30aMdjgePHz8eu56SVTBbgxJMliQ3Oauwg0QHxXE2Ez/EIReLdQj42Gzb4CLS0YJD9xUx7bsi0vJi5mUbW1QzL0h0PFk17rtiIPfJk52MB48fPx67npJJwyrBa2RCCQRTbGZSPCxTPOiND4G2pYyOQ4h4jINIJh5wFU1NFZt+IsZ59LSnDqBjZ2awbOku+yInunLcd8VA7rNnOxkPHj9+PGY9B0MWJJNozOJmlglvDMXDEozdhQWbgs/U6oBanGzLrdSNNnZFjOkmbi5bNt1lX7JLLhn3vXAg9/h4y/Hg8ePHI9dzQMEkWCgdRfYykYKnkP7D4rIujsujaKPBsB54vE2TS00ccvFY/Tth7JXeq1hz+qgVy04sAJawTsvOknHfCwdyT062HA8eP348Zj0vdoXF4pilKa2BROed+9fyw9rWRXeTFXESMOanvDZfJuJaSXouQdMdDJZtekZcLLvEeK04d8m474UDuaenW44Hjx8/Xns9YYqZpszGWB3AN/4VHw+k7WSFtJ3Qicuqb/NlVmgXWsxh570xg2UwxUw3WfO6B5nOuO8aA7lnZxuPB48fPx6znm1i4bsfcbaptF3zNT78eFPtwi1OaCNOqp1x3zUGcs/PN++AGD1+fMXrSVm2baTtPhPahbPhA71wIHd2bXzRa69nG+3CraTtPivahV/55tXWg8fyRY/9AdsY8VbSdp8V7cKrrgdfM//z6ILQFtJ2nxHtwmuoB4/kf74+gLeRtvvMaBdeSz34+vifx0YG20jbfTa0C6+tHrwe//NmOG0L8EbSdp8R7cLrrQe/996O+ai3ujQOskpTNULa7jOjXXj99eCd8lHvoFiwsbTdZ0a78PrrwTvlo966pLuRtB2fFe3Cm6oHP9kNH/W2FryxtN1nTLvwRurBO+Kj3pWXHidtx2dFu/Bm68Fb81HvykuPlrb7LGkX3mw9eGs+6h1Y8MbSdjegXcguQLjmevDpTQLMxtJ2N6NdyBZu9AbrwVvwUW+LbteULUpCdqm0HTelXbhNPe8G68Gb8lFvVfYfSNuxvrTdTWoXbozAzdaDZzfkorOj1oxVxlIMlpSIlpLrt8D4hrQL17z+c3h6hU/wv4Q/utps4+bm+6P/hIcf0JwQ5oQGPBL0eKPTYEXTW+eL/2DKn73J9BTXYANG57hz1cEMviVf/4tf5b/6C5pTQkMIWoAq7hTpOJjtAM4pxKu5vg5vXeUrtI09/Mo/5H+4z+Mp5xULh7cEm2QbRP2tFIKR7WM3fPf/jZ3SWCqLM2l4NxID5zB72HQXv3jj/8mLR5xXNA5v8EbFQEz7PpRfl1+MB/hlAN65qgDn3wTgH13hK7T59bmP+NIx1SHHU84nLOITt3iVz8mNO+lPrjGAnBFqmioNn1mTyk1ta47R6d4MrX7tjrnjYUpdUbv2rVr6YpVfsGG58AG8Ah9eyUN8CX4WfgV+G8LVWPDGb+Zd4cU584CtqSbMKxauxTg+dyn/LkVgA+IR8KHtejeFKRtTmLLpxN6mYVLjYxwXf5x2VofiZcp/lwKk4wGOpYDnoIZPdg/AAbwMfx0+ge9dgZvYjuqKe4HnGnykYo5TvJbG0Vj12JagRhwKa44H95ShkZa5RyLGGdfYvG7aw1TsF6iapPAS29mNS3NmsTQZCmgTzFwgL3upCTgtBTRwvGMAKrgLn4evwin8+afJRcff+8izUGUM63GOOuAs3tJkw7J4kyoNreqrpO6cYLQeFUd7TTpr5YOTLc9RUUogUOVJQ1GYJaFLAW0oTmKyYS46ZooP4S4EON3xQ5zC8/CX4CnM4c1PE8ApexpoYuzqlP3d4S3OJP8ZDK7cKWNaTlqmgDiiHwl1YsE41w1zT4iRTm3DBqxvOUsbMKKDa/EHxagtnta072ejc3DOIh5ojvh8l3tk1JF/AV6FU6jh3U8HwEazLgdCLYSQ+MYiAI2ltomkzttUb0gGHdSUUgsIYjTzLG3mObX4FBRaYtpDVNZrih9TgTeYOBxsEnN1gOCTM8Bsw/ieMc75w9kuAT6A+/AiHGvN/+Gn4KRkiuzpNNDYhDGFndWRpE6SVfm8U5bxnSgVV2jrg6JCKmneqey8VMFgq2+AM/i4L4RUbfSi27lNXZ7R7W9RTcq/q9fk4Xw3AMQd4I5ifAZz8FcVtm9SAom/dyN4lczJQW/kC42ZrHgcCoIf1oVMKkVItmMBi9cOeNHGLqOZk+QqQmrbc5YmYgxELUUN35z2iohstgfLIFmcMV7s4CFmI74L9+EFmGsi+tGnAOD4Yk9gIpo01Y4cA43BWGygMdr4YZekG3OBIUXXNukvJS8tqa06e+lSDCtnqqMFu6hWHXCF+WaYt64m9QBmNxi7Ioy7D+fa1yHw+FMAcPt7SysFLtoG4PXAk7JOA3aAxBRqUiAdU9Yp5lK3HLSRFtOim0sa8euEt08xvKjYjzeJ2GU7YawexrnKI9tmobInjFXCewpwriY9+RR4aaezFhMhGCppKwom0ChrgFlKzyPKkGlTW1YQrE9HJqu8hKGgMc6hVi5QRq0PZxNfrYNgE64utmRv6KKHRpxf6VDUaOvNP5jCEx5q185My/7RKz69UQu2im5k4/eownpxZxNLwiZ1AZTO2ZjWjkU9uaB2HFn6Q3u0JcsSx/qV9hTEApRzeBLDJQXxYmTnq7bdLa3+uqFrxLJ5w1TehnNHx5ECvCh2g2c3hHH5YsfdaSKddztfjQ6imKFGSyFwlLzxEGPp6r5IevVjk1AMx3wMqi1NxDVjLBiPs9tbsCkIY5we5/ML22zrCScFxnNtzsr9Wcc3CnD+pYO+4VXXiDE0oc/vQQ/fDK3oPESJMYXNmJa/DuloJZkcTpcYE8lIH8Dz8DJMiynNC86Mb2lNaaqP/+L7f2fcE/yP7/Lde8xfgSOdMxvOixZf/9p3+M4hT1+F+zApxg9XfUvYjc8qX2lfOOpK2gNRtB4flpFu9FTKCp2XJRgXnX6olp1zyYjTKJSkGmLE2NjUr1bxFM4AeAAHBUFIeSLqXR+NvH/M9fOnfHzOD2vCSyQJKzfgsCh+yi/Mmc35F2fUrw7miW33W9hBD1vpuUojFphIyvg7aTeoymDkIkeW3XLHmguMzbIAJejN6B5MDrhipE2y6SoFRO/AK/AcHHZHNIfiWrEe/C6cr3f/yOvrQKB+zMM55/GQdLDsR+ifr5Fiuu+/y+M78LzOE5dsNuXC3PYvYWd8NXvphLSkJIasrlD2/HOqQ+RjcRdjKTGWYhhVUm4yxlyiGPuMsZR7sMCHUBeTuNWA7if+ifXgc/hovftHXs/DV+Fvwe+f8shzMiMcweFgBly3//vwJfg5AN4450fn1Hd1Rm1aBLu22Dy3y3H2+OqMemkbGZ4jozcDjJf6596xOLpC0eMTHbKnxLxH27uZ/bMTGs2jOaMOY4m87CfQwF0dw53oa1k80JRuz/XgS+8fX3N9Af4qPIMfzKgCp4H5TDGe9GGeFPzSsZz80SlPTxXjgwJmC45njzgt2vbQ4b4OAdUK4/vWhO8d8v6EE8fMUsfakXbPpFJeLs2ubM/qdm/la3WP91uWhxXHjoWhyRUq2iJ/+5mA73zwIIo+LoZ/SgvIRjAd1IMvvn98PfgOvAJfhhm8scAKVWDuaRaK8aQ9f7vuPDH6Bj47ZXau7rqYJ66mTDwEDU6lLbCjCK0qTXyl5mnDoeNRxanj3FJbaksTk0faXxHxLrssgPkWB9LnA/MFleXcJozzjwsUvUG0X/QCve51qkMDXp9mtcyOy3rwBfdvVJK7D6/ACSzg3RoruIq5UDeESfEmVclDxnniU82vxMLtceD0hGZWzBNPMM/jSPne2OVatiTKUpY5vY7gc0LdUAWeWM5tH+O2I66AOWw9xT2BuyRVLGdoDHUsVRXOo/c+ZdRXvFfnxWyIV4upFLCl9eAL7h8Zv0QH8Ry8pA2cHzQpGesctVA37ZtklBTgHjyvdSeKY/RZw/kJMk0Y25cSNRWSigQtlULPTw+kzuJPeYEkXjQRpoGZobYsLF79pyd1dMRHInbgFTZqNLhDqiIsTNpoex2WLcy0/X6rHcdMMQvFSd5dWA++4P7xv89deACnmr36uGlL69bRCL6BSZsS6c0TU2TKK5gtWCzgAOOwQcurqk9j8whvziZSMLcq5hbuwBEsYjopUBkqw1yYBGpLA97SRElEmx5MCInBY5vgLk94iKqSWmhIGmkJ4Bi9m4L645J68LyY4wsFYBfUg5feP/6gWWm58IEmKQM89hq7KsZNaKtP5TxxrUZZVkNmMJtjbKrGxLNEbHPJxhqy7lAmbC32ZqeF6lTaknRWcYaFpfLUBh/rwaQycCCJmW15Kstv6jRHyJFry2C1ahkkIW0LO75s61+owxK1y3XqweX9m5YLM2DPFeOjn/iiqCKJ+yKXF8t5Yl/kNsqaSCryxPq5xWTFIaP8KSW0RYxqupaUf0RcTNSSdJZGcKYdYA6kdtrtmyBckfKXwqk0pHpUHlwWaffjNRBYFPUDWa8e3Lt/o0R0CdisKDM89cX0pvRHEfM8ca4t0s2Xx4kgo91MPQJ/0c9MQYq0co8MBh7bz1fio0UUHLR4aAIOvOmoYO6kwlEVODSSTliWtOtH6sPkrtctF9ZtJ9GIerBskvhdVS5cFNv9s1BU0AbdUgdK4FG+dRnjFmDTzniRMdZO1QhzMK355vigbdkpz9P6qjUGE5J2qAcXmwJ20cZUiAD0z+pGMx6xkzJkmEf40Hr4qZfVg2XzF9YOyoV5BjzVkUJngKf8lgNYwKECEHrCNDrWZzMlflS3yBhr/InyoUgBc/lKT4pxVrrC6g1YwcceK3BmNxZcAtz3j5EIpqguh9H6wc011YN75cKDLpFDxuwkrPQmUwW4KTbj9mZTwBwLq4aQMUZbHm1rylJ46dzR0dua2n3RYCWZsiHROeywyJGR7mXKlpryyCiouY56sFkBWEnkEB/raeh/Sw4162KeuAxMQpEkzy5alMY5wamMsWKKrtW2WpEWNnReZWONKWjrdsKZarpFjqCslq773PLmEhM448Pc3+FKr1+94vv/rfw4tEcu+lKTBe4kZSdijBrykwv9vbCMPcLQTygBjzVckSLPRVGslqdunwJ4oegtFOYb4SwxNgWLCmD7T9kVjTv5YDgpo0XBmN34Z/rEHp0sgyz7lngsrm4lvMm2Mr1zNOJYJ5cuxuQxwMGJq/TP5emlb8fsQBZviK4t8hFL+zbhtlpwaRSxQRWfeETjuauPsdGxsBVdO7nmP4xvzSoT29pRl7kGqz+k26B3Oy0YNV+SXbbQas1ctC/GarskRdFpKczVAF1ZXnLcpaMuzVe6lZ2g/1ndcvOVgRG3sdUAY1bKD6achijMPdMxV4muKVorSpiDHituH7rSTs7n/4y5DhRXo4FVBN4vO/zbAcxhENzGbHCzU/98Mcx5e7a31kWjw9FCe/zNeYyQjZsWb1uc7U33pN4Mji6hCLhivqfa9Ss6xLg031AgfesA/l99m9fgvnaF9JoE6bYKmkGNK3aPbHB96w3+DnxFm4hs0drLsk7U8kf/N/CvwQNtllna0rjq61sH8L80HAuvwH1tvBy2ChqWSCaYTaGN19sTvlfzFD6n+iKTbvtayfrfe9ueWh6GJFoxLdr7V72a5ZpvHcCPDzma0wTO4EgbLyedxstO81n57LYBOBzyfsOhUKsW1J1BB5vr/tz8RyqOFylQP9Tvst2JALsC5lsH8PyQ40DV4ANzYa4dedNiKNR1s+x2wwbR7q4/4cTxqEk4LWDebfisuo36JXLiWFjOtLrlNWh3K1rRS4xvHcDNlFnNmWBBAl5SWaL3oPOfnvbr5pdjVnEaeBJSYjuLEkyLLsWhKccadmOphZkOPgVdalj2QpSmfOsADhMWE2ZBu4+EEJI4wKTAuCoC4xwQbWXBltpxbjkXJtKxxabo9e7tyhlgb6gNlSbUpMh+l/FaqzVwewGu8BW1Zx7pTpQDJUjb8tsUTW6+GDXbMn3mLbXlXJiGdggxFAoUrtPS3wE4Nk02UZG2OOzlk7fRs7i95QCLo3E0jtrjnM7SR3uS1p4qtS2nJ5OwtQVHgOvArLBFijZUV9QtSl8dAY5d0E0hM0w3HS2DpIeB6m/A1+HfhJcGUq4sOxH+x3f5+VO+Ds9rYNI7zPXOYWPrtf8bYMx6fuOAX5jzNR0PdsuON+X1f7EERxMJJoU6GkTEWBvVolVlb5lh3tKCg6Wx1IbaMDdJ+9sUCc5KC46hKGCk3IVOS4TCqdBNfUs7Kd4iXf2RjnT/LLysJy3XDcHLh/vde3x8DoGvwgsa67vBk91G5Pe/HbOe7xwym0NXbtiuuDkGO2IJDh9oQvJ4cY4vdoqLDuoH9Zl2F/ofsekn8lkuhIlhQcffUtSjytFyp++p6NiE7Rqx/lodgKVoceEp/CP4FfjrquZaTtj2AvH5K/ywpn7M34K/SsoYDAdIN448I1/0/wveW289T1/lX5xBzc8N5IaHr0XMOQdHsIkDuJFifj20pBm5jzwUv9e2FhwRsvhAbalCIuIw3bhJihY3p6nTFFIZgiSYjfTf3aXuOjmeGn4bPoGvwl+CFzTRczBIuHBEeImHc37/lGfwZR0cXzVDOvaKfNHvwe+suZ771K/y/XcBlsoN996JpBhoE2toYxOznNEOS5TJc6Id5GEXLjrWo+LEWGNpPDU4WAwsIRROu+1vM+0oW37z/MBN9kqHnSArwPfgFJ7Cq/Ai3Ie7g7ncmI09v8sjzw9mzOAEXoIHxURueaAce5V80f/DOuuZwHM8vsMb5wBzOFWM7wymTXPAEvm4vcFpZ2ut0VZRjkiP2MlmLd6DIpbGSiHOjdnUHN90hRYmhTnmvhzp1iKDNj+b7t5hi79lWGwQ+HN9RsfFMy0FXbEwhfuczKgCbyxYwBmcFhhvo/7a44v+i3XWcwDP86PzpGQYdWh7csP5dBvZ1jNzdxC8pBGuxqSW5vw40nBpj5JhMwvOzN0RWqERHMr4Lv1kWX84xLR830G3j6yqZ1a8UstTlW+qJPOZ+sZ7xZPKTJLhiNOAFd6tk+jrTH31ncLOxid8+nzRb128HhUcru/y0Wn6iT254YPC6FtVSIMoW2sk727AhvTtrWKZTvgsmckfXYZWeNRXx/3YQ2OUxLDrbHtN11IwrgXT6c8dATDwLniYwxzO4RzuQqTKSC5gAofMZ1QBK3zQ4JWobFbcvJm87FK+6JXrKahLn54m3p+McXzzYtP8VF/QpJuh1OwieElEoI1pRxPS09FBrkq2tWCU59+HdhNtTIqKm8EBrw2RTOEDpG3IKo2Y7mFdLm3ZeVjYwVw11o/oznceMve4CgMfNym/utA/d/ILMR7gpXzRy9eDsgLcgbs8O2Va1L0zzIdwGGemTBuwROHeoMShkUc7P+ISY3KH5ZZeWqO8mFTxQYeXTNuzvvK5FGPdQfuu00DwYFY9dyhctEt+OJDdnucfpmyhzUJzfsJjr29l8S0bXBfwRS9ZT26tmMIdZucch5ZboMz3Nio3nIOsYHCGoDT4kUA9MiXEp9Xsui1S8th/kbWIrMBxDGLodWUQIWcvnXy+9M23xPiSMOiRPqM+YMXkUN3gXFrZJwXGzUaMpJfyRS9ZT0lPe8TpScuRlbMHeUmlaKDoNuy62iWNTWNFYjoxFzuJs8oR+RhRx7O4SVNSXpa0ZJQ0K1LAHDQ+D9IepkMXpcsq5EVCvClBUIzDhDoyKwDw1Lc59GbTeORivugw1IcuaEOaGWdNm+Ps5fQ7/tm0DjMegq3yM3vb5j12qUId5UZD2oxDSEWOZMSqFl/W+5oynWDa/aI04tJRQ2eTXusg86SQVu/nwSYwpW6wLjlqIzwLuxGIvoAvul0PS+ZNz0/akp/pniO/8JDnGyaCkzbhl6YcqmK/69prxPqtpx2+Km9al9sjL+rwMgHw4jE/C8/HQ3m1vBuL1fldbzd8mOueVJ92syqdEY4KJjSCde3mcRw2TA6szxedn+zwhZMps0XrqEsiUjnC1hw0TELC2Ek7uAAdzcheXv1BYLagspxpzSAoZZUsIzIq35MnFQ9DOrlNB30jq3L4pkhccKUAA8/ocvN1Rzx9QyOtERs4CVsJRK/DF71kPYrxYsGsm6RMh4cps5g1DOmM54Ly1ii0Hd3Y/BMk8VWFgBVmhqrkJCPBHAolwZaWzLR9Vb7bcWdX9NyUYE+uB2BKfuaeBUcjDljbYVY4DdtsVWvzRZdWnyUzDpjNl1Du3aloAjVJTNDpcIOVVhrHFF66lLfJL1zJr9PQ2nFJSBaKoDe+sAvLufZVHVzYh7W0h/c6AAZ+7Tvj6q9j68G/cTCS/3n1vLKHZwNi+P+pS0WkZNMBMUl+LDLuiE4omZy71r3UFMwNJV+VJ/GC5ixVUkBStsT4gGKh0Gm4Oy3qvq7Lbmq24nPdDuDR9deR11XzP4vFu3TYzfnIyiSVmgizUYGqkIXNdKTY9pgb9D2Ix5t0+NHkVzCdU03suWkkVZAoCONCn0T35gAeW38de43mf97sMOpSvj4aa1KYUm58USI7Wxxes03bAZdRzk6UtbzMaCQ6IxO0dy7X+XsjoD16hpsBeGz9dfzHj+R/Hp8nCxZRqkEDTaCKCSywjiaoMJ1TITE9eg7Jqnq8HL6gDwiZb0u0V0Rr/rmvqjxKuaLCX7ZWXTvAY+uvm3z8CP7nzVpngqrJpZKwWnCUjIviYVlirlGOzPLI3SMVyp/elvBUjjDkNhrtufFFErQ8pmdSlbK16toBHlt/HV8uHMX/vEGALkV3RJREiSlopxwdMXOZPLZ+ix+kAHpMKIk8UtE1ygtquttwxNhphrIZ1IBzjGF3IIGxGcBj6q8bHJBG8T9vdsoWrTFEuebEZuVxhhClH6P5Zo89OG9fwHNjtNQTpD0TG9PJLEYqvEY6Rlxy+ZZGfL0Aj62/bnQCXp//eeM4KzfQVJbgMQbUjlMFIm6TpcfWlZje7NBSV6IsEVmumWIbjiloUzQX9OzYdo8L1wjw2PrrpimONfmfNyzKklrgnEkSzT5QWYQW40YShyzqsRmMXbvVxKtGuYyMKaU1ugenLDm5Ily4iT14fP11Mx+xJv+zZ3MvnfdFqxU3a1W/FTB4m3Qfsyc1XUcdVhDeUDZXSFHHLQj/Y5jtC7ZqM0CXGwB4bP11i3LhOvzPGygYtiUBiwQV/4wFO0majijGsafHyRLu0yG6q35cL1rOpVxr2s5cM2jJYMCdc10Aj6q/blRpWJ//+dmm5psMl0KA2+AFRx9jMe2WbC4jQxnikd4DU8TwUjRVacgdlhmr3bpddzuJ9zXqr2xnxJfzP29RexdtjDVZqzkqa6PyvcojGrfkXiJ8SEtml/nYskicv0ivlxbqjemwUjMw5evdg8fUX9nOiC/lf94Q2i7MURk9nW1MSj5j8eAyV6y5CN2S6qbnw3vdA1Iwq+XOSCl663udN3IzLnrt+us25cI1+Z83SXQUldqQq0b5XOT17bGpLd6ssN1VMPf8c+jG8L3NeCnMdF+Ra3fRa9dft39/LuZ/3vwHoHrqGmQFafmiQw6eyzMxS05K4bL9uA+SKUQzCnSDkqOGokXyJvbgJ/BHI+qvY69//4rl20NsmK2ou2dTsyIALv/91/8n3P2Aao71WFGi8KKv1fRC5+J67Q/507/E/SOshqN5TsmYIjVt+kcjAx98iz/4SaojbIV1rexE7/C29HcYD/DX4a0rBOF5VTu7omsb11L/AWcVlcVZHSsqGuXLLp9ha8I//w3Mv+T4Ew7nTBsmgapoCrNFObIcN4pf/Ob/mrvHTGqqgAupL8qWjWPS9m/31jAe4DjA+4+uCoQoT/zOzlrNd3qd4SdphFxsUvYwGWbTWtISc3wNOWH+kHBMfc6kpmpwPgHWwqaSUG2ZWWheYOGQGaHB+eQ/kn6b3pOgLV+ODSn94wDvr8Bvb70/LLuiPPEr8OGVWfDmr45PZyccEmsVXZGe1pRNX9SU5+AVQkNTIVPCHF/jGmyDC9j4R9LfWcQvfiETmgMMUCMN1uNCakkweZsowdYobiMSlnKA93u7NzTXlSfe+SVbfnPQXmg9LpYAQxpwEtONyEyaueWM4FPjjyjG3uOaFmBTWDNgBXGEiQpsaWhnAqIijB07Dlsy3fUGeP989xbWkyf+FF2SNEtT1E0f4DYYVlxFlbaSMPIRMk/3iMU5pME2SIWJvjckciebkQuIRRyhUvkHg/iUljG5kzVog5hV7vIlCuBrmlhvgPfNHQM8lCf+FEGsYbMIBC0qC9a0uuy2wLXVbLBaP5kjHokCRxapkQyzI4QEcwgYHRZBp+XEFTqXFuNVzMtjXLJgX4gAid24Hjwc4N3dtVSe+NNiwTrzH4WVUOlDobUqr1FuAgYllc8pmzoVrELRHSIW8ViPxNy4xwjBpyR55I6J220qQTZYR4guvUICJiSpr9gFFle4RcF/OMB7BRiX8sSfhpNSO3lvEZCQfLUVTKT78Ek1LRLhWN+yLyTnp8qWUZ46b6vxdRGXfHVqx3eI75YaLa4iNNiK4NOW7wPW6lhbSOF9/M9qw8e/aoB3d156qTzxp8pXx5BKAsYSTOIIiPkp68GmTq7sZtvyzBQaRLNxIZ+paozHWoLFeExIhRBrWitHCAHrCF7/thhD8JhYz84wg93QRV88wLuLY8zF8sQ36qF1J455bOlgnELfshKVxYOXKVuKx0jaj22sczTQqPqtV/XDgpswmGTWWMSDw3ssyUunLLrVPGjYRsH5ggHeHSWiV8kT33ycFSfMgkoOK8apCye0J6VW6GOYvffgU9RWsukEi2kUV2nl4dOYUzRik9p7bcA4ggdJ53LxKcEe17B1R8eqAd7dOepV8sTXf5lhejoL85hUdhDdknPtKHFhljOT+bdq0hxbm35p2nc8+Ja1Iw+tJykgp0EWuAAZYwMVwac5KzYMslhvgHdHRrxKnvhTYcfKsxTxtTETkjHO7rr3zjoV25lAQHrqpV7bTiy2aXMmUhTBnKS91jhtR3GEoF0oLnWhWNnYgtcc4N0FxlcgT7yz3TgNIKkscx9jtV1ZKpWW+Ub1tc1eOv5ucdgpx+FJy9pgbLE7xDyXb/f+hLHVGeitHOi6A7ybo3sF8sS7w7cgdk0nJaOn3hLj3uyD0Zp5pazFIUXUpuTTU18d1EPkDoX8SkmWTnVIozEdbTcZjoqxhNHf1JrSS/AcvHjZ/SMHhL/7i5z+POsTUh/8BvNfYMTA8n+yU/MlTZxSJDRStqvEuLQKWwDctMTQogUDyQRoTQG5Kc6oQRE1yV1jCA7ri7jdZyK0sYTRjCR0Hnnd+y7nHxNgTULqw+8wj0mQKxpYvhjm9uSUxg+TTy7s2GtLUGcywhXSKZN275GsqlclX90J6bRI1aouxmgL7Q0Nen5ziM80SqMIo8cSOo+8XplT/5DHNWsSUr/6lLN/QQ3rDyzLruEW5enpf7KqZoShEduuSFOV7DLX7Ye+GmXb6/hnNNqKsVXuMDFpb9Y9eH3C6NGEzuOuI3gpMH/I6e+zDiH1fXi15t3vA1czsLws0TGEtmPEJdiiFPwlwKbgLHAFk4P6ZyPdymYYHGE0dutsChQBl2JcBFlrEkY/N5bQeXQ18gjunuMfMfsBlxJSx3niO485fwO4fGD5T/+3fPQqkneWVdwnw/3bMPkW9Wbqg+iC765Zk+xcT98ibKZc2EdgHcLoF8cSOo/Oc8fS+OyEULF4g4sJqXVcmfMfsc7A8v1/yfGXmL9I6Fn5pRwZhsPv0TxFNlAfZCvG+Oohi82UC5f/2IsJo0cTOm9YrDoKhFPEUr/LBYTUNht9zelHXDqwfPCIw4owp3mOcIQcLttWXFe3VZ/j5H3cIc0G6oPbCR+6Y2xF2EC5cGUm6wKC5tGEzhsWqw5hNidUiKX5gFWE1GXh4/Qplw4sVzOmx9QxU78g3EF6wnZlEN4FzJ1QPSLEZz1KfXC7vd8ssGdIbNUYpVx4UapyFUHzJoTOo1McSkeNn1M5MDQfs4qQuhhX5vQZFw8suwWTcyYTgioISk2YdmkhehG4PkE7w51inyAGGaU+uCXADabGzJR1fn3lwkty0asIo8cROm9Vy1g0yDxxtPvHDAmpu+PKnM8Ix1wwsGw91YJqhteaWgjYBmmQiebmSpwKKzE19hx7jkzSWOm66oPbzZ8Yj6kxVSpYjVAuvLzYMCRo3oTQecOOjjgi3NQ4l9K5/hOGhNTdcWVOTrlgYNkEXINbpCkBRyqhp+LdRB3g0OU6rMfW2HPCFFMV9nSp+uB2woepdbLBuJQyaw/ZFysXrlXwHxI0b0LovEkiOpXGA1Ijagf+KUNC6rKNa9bQnLFqYNkEnMc1uJrg2u64ELPBHpkgWbmwKpJoDhMwNbbGzAp7Yg31wS2T5rGtzit59PrKhesWG550CZpHEzpv2NGRaxlNjbMqpmEIzygJqQfjypycs2pg2cS2RY9r8HUqkqdEgKTWtWTKoRvOBPDYBltja2SO0RGjy9UHtxwRjA11ujbKF+ti5cIR9eCnxUg6owidtyoU5tK4NLji5Q3HCtiyF2IqLGYsHViOXTXOYxucDqG0HyttqYAKqYo3KTY1ekyDXRAm2AWh9JmsVh/ccg9WJ2E8YjG201sPq5ULxxX8n3XLXuMInbft2mk80rRGjCGctJ8/GFdmEQ9Ug4FlE1ll1Y7jtiraqm5Fe04VV8lvSVBL8hiPrfFVd8+7QH3Qbu2ipTVi8cvSGivc9cj8yvH11YMHdNSERtuOslM97feYFOPKzGcsI4zW0YGAbTAOaxCnxdfiYUmVWslxiIblCeAYr9VYR1gM7GmoPrilunSxxeT3DN/2eBQ9H11+nk1adn6VK71+5+Jfct4/el10/7KBZfNryUunWSCPxPECk1rdOv1WVSrQmpC+Tl46YD3ikQYcpunSQgzVB2VHFhxHVGKDgMEY5GLlQnP7FMDzw7IacAWnO6sBr12u+XanW2AO0wQ8pknnFhsL7KYIqhkEPmEXFkwaN5KQphbkUmG72wgw7WSm9RiL9QT925hkjiVIIhphFS9HKI6/8QAjlpXqg9W2C0apyaVDwKQwrwLY3j6ADR13ZyUNByQXHQu6RY09Hu6zMqXRaNZGS/KEJs0cJEe9VH1QdvBSJv9h09eiRmy0V2uJcqHcShcdvbSNg5fxkenkVprXM9rDVnX24/y9MVtncvbKY706anNl3ASll9a43UiacVquXGhvq4s2FP62NGKfQLIQYu9q1WmdMfmUrDGt8eDS0cXozH/fjmUH6Jruvm50hBDSaEU/2Ru2LEN/dl006TSc/g7tfJERxGMsgDUEr104pfWH9lQaN+M4KWQjwZbVc2rZVNHsyHal23wZtIs2JJqtIc/WLXXRFCpJkfE9jvWlfFbsNQ9pP5ZBS0zKh4R0aMFj1IjTcTnvi0Zz2rt7NdvQb2mgbju1plsH8MmbnEk7KbK0b+wC2iy3aX3szW8xeZvDwET6hWZYwqTXSSG+wMETKum0Dq/q+x62gt2ua2ppAo309TRk9TPazfV3qL9H8z7uhGqGqxNVg/FKx0HBl9OVUORn8Q8Jx9gFttGQUDr3tzcXX9xGgN0EpzN9mdZ3GATtPhL+CjxFDmkeEU6x56kqZRusLzALXVqkCN7zMEcqwjmywDQ6OhyUe0Xao1Qpyncrg6wKp9XfWDsaZplElvQ/b3sdweeghorwBDlHzgk1JmMc/wiERICVy2VJFdMjFuLQSp3S0W3+sngt2njwNgLssFGVQdJ0tu0KH4ky1LW4yrbkuaA6Iy9oz/qEMMXMMDWyIHhsAyFZc2peV9hc7kiKvfULxCl9iddfRK1f8kk9qvbdOoBtOg7ZkOZ5MsGrSHsokgLXUp9y88smniwWyuFSIRVmjplga3yD8Uij5QS1ZiM4U3Qw5QlSm2bXjFe6jzzBFtpg+/YBbLAWG7OPynNjlCw65fukGNdkJRf7yM1fOxVzbxOJVocFoYIaGwH22mIQkrvu1E2nGuebxIgW9U9TSiukPGU+Lt++c3DJPKhyhEEbXCQLUpae2exiKy6tMPe9mDRBFCEMTWrtwxN8qvuGnt6MoihKWS5NSyBhbH8StXoAz8PLOrRgLtOT/+4vcu+7vDLnqNvztOq7fmd8sMmY9Xzn1zj8Dq8+XVdu2Nv0IIySgEdQo3xVHps3Q5i3fLFsV4aiqzAiBhbgMDEd1uh8qZZ+lwhjkgokkOIv4xNJmyncdfUUzgB4oFMBtiu71Xumpz/P+cfUP+SlwFExwWW62r7b+LSPxqxn/gvMZ5z9C16t15UbNlq+jbGJtco7p8wbYlL4alSyfWdeuu0j7JA3JFNuVAwtst7F7FhWBbPFNKIUORndWtLraFLmMu7KFVDDOzqkeaiN33YAW/r76wR4XDN/yN1z7hejPau06EddkS/6XThfcz1fI/4K736fO48vlxt2PXJYFaeUkFS8U15XE3428xdtn2kc8GQlf1vkIaNRRnOMvLTWrZbElEHeLWi1o0dlKPAh1MVgbbVquPJ5+Cr8LU5/H/+I2QlHIU2ClXM9G8v7Rr7oc/hozfUUgsPnb3D+I+7WF8kNO92GY0SNvuxiE+2Bt8prVJTkzE64sfOstxuwfxUUoyk8VjcTlsqe2qITSFoSj6Epd4KsT6BZOWmtgE3hBfir8IzZDwgV4ZTZvD8VvPHERo8v+vL1DASHTz/i9OlKueHDjK5Rnx/JB1Vb1ioXdBra16dmt7dgik10yA/FwJSVY6XjA3oy4SqM2frqDPPSRMex9qs3XQtoWxMj7/Er8GWYsXgjaVz4OYumP2+9kbxvny/6kvWsEBw+fcb5bInc8APdhpOSs01tEqIkoiZjbAqKMruLbJYddHuHFRIyJcbdEdbl2sVLaySygunutBg96Y2/JjKRCdyHV+AEFtTvIpbKIXOamknYSiB6KV/0JetZITgcjjk5ZdaskBtWO86UF0ap6ozGXJk2WNiRUlCPFir66lzdm/SLSuK7EUdPz8f1z29Skq6F1fXg8+5UVR6bszncP4Tn4KUkkdJ8UFCY1zR1i8RmL/qQL3rlei4THG7OODlnKko4oI01kd3CaM08Ia18kC3GNoVaO9iDh+hWxSyTXFABXoau7Q6q9OxYg/OVEMw6jdbtSrJ9cBcewGmaZmg+bvkUnUUaGr+ZfnMH45Ivevl61hMcXsxYLFTu1hTm2zViCp7u0o5l+2PSUh9bDj6FgYypufBDhqK2+oXkiuHFHR3zfj+9PtA8oR0xnqX8qn+sx3bFODSbbF0X8EUvWQ8jBIcjo5bRmLOljDNtcqNtOe756h3l0VhKa9hDd2l1eqmsnh0MNMT/Cqnx6BInumhLT8luljzQ53RiJeA/0dxe5NK0o2fA1+GLXr6eNQWHNUOJssQaTRlGpLHKL9fD+IrQzTOMZS9fNQD4AnRNVxvTdjC+fJdcDDWQcyB00B0t9BDwTxXgaAfzDZ/DBXzRnfWMFRwuNqocOmX6OKNkY63h5n/fFcB28McVHqnXZVI27K0i4rDLNE9lDKV/rT+udVbD8dFFu2GGZ8mOt0kAXcoX3ZkIWVtw+MNf5NjR2FbivROHmhV1/pj2egv/fMGIOWTIWrV3Av8N9imV9IWml36H6cUjqEWNv9aNc+veb2sH46PRaHSuMBxvtW+twxctq0z+QsHhux8Q7rCY4Ct8lqsx7c6Sy0dl5T89rIeEuZKoVctIk1hNpfavER6yyH1Vvm3MbsUHy4ab4hWr/OZPcsRBphnaV65/ZcdYPNNwsjN/djlf9NqCw9U5ExCPcdhKxUgLSmfROpLp4WSUr8ojdwbncbvCf+a/YzRaEc6QOvXcGO256TXc5Lab9POvB+AWY7PigWYjzhifbovuunzRawsO24ZqQQAqguBtmpmPB7ysXJfyDDaV/aPGillgz1MdQg4u5MYaEtBNNHFjkRlSpd65lp4hd2AVPTfbV7FGpyIOfmNc/XVsPfg7vzaS/3nkvLL593ANLvMuRMGpQIhiF7kUEW9QDpAUbTWYBcbp4WpacHHY1aacqQyjGZS9HI3yCBT9kUZJhVOD+zUDvEH9ddR11fzPcTDQ5TlgB0KwqdXSavk9BC0pKp0WmcuowSw07VXmXC5guzSa4p0UvRw2lbDiYUx0ExJJRzWzi6Gm8cnEkfXXsdcG/M/jAJa0+bmCgdmQ9CYlNlSYZOKixmRsgiFxkrmW4l3KdFKv1DM8tk6WxPYJZhUUzcd8Kdtgrw/gkfXXDT7+avmfVak32qhtkg6NVdUS5wgkru1YzIkSduTW1FDwVWV3JQVJVuieTc0y4iDpFwc7/BvSalvKdQM8sv662cevz/+8sQVnjVAT0W2wLllw1JiMhJRxgDjCjLQsOzSFSgZqx7lAW1JW0e03yAD3asC+GD3NbQhbe+mN5GXH1F83KDOM4n/e5JIuH4NpdQARrFPBVptUNcjj4cVMcFSRTE2NpR1LEYbYMmfWpXgP9KejaPsLUhuvLCsVXznAG9dfx9SR1ud/3hZdCLHb1GMdPqRJgqDmm76mHbvOXDtiO2QPUcKo/TWkQ0i2JFXpBoo7vij1i1Lp3ADAo+qvG3V0rM//vFnnTE4hxd5Ka/Cor5YEdsLVJyKtDgVoHgtW11pWSjolPNMnrlrVj9Fv2Qn60twMwKPqr+N/wvr8z5tZcDsDrv06tkqyzESM85Ycv6XBWA2birlNCXrI6VbD2lx2L0vQO0QVTVVLH4SE67fgsfVXv8n7sz7/85Z7cMtbE6f088wSaR4kCkCm10s6pKbJhfqiUNGLq+0gLWC6eUAZFPnLjwqtKd8EwGvWX59t7iPW4X/eAN1svgRVSY990YZg06BD1ohLMtyFTI4pKTJsS9xREq9EOaPWiO2gpms7397x6nQJkbh+Fz2q/rqRROX6/M8bJrqlVW4l6JEptKeUFuMYUbtCQ7CIttpGc6MY93x1r1vgAnRXvY5cvwWPqb9uWQm+lP95QxdNMeWhOq1x0Db55C7GcUv2ZUuN6n8iKzsvOxibC//Yfs9Na8r2Rlz02vXXDT57FP/zJi66/EJSmsJKa8QxnoqW3VLQ+jZVUtJwJ8PNX1NQCwfNgdhhHD9on7PdRdrdGPF28rJr1F+3LBdeyv+8yYfLoMYet1vX4upNAjVvwOUWnlNXJXlkzk5Il6kqeoiL0C07qno+/CYBXq/+utlnsz7/Mzvy0tmI4zm4ag23PRN3t/CWryoUVJGm+5+K8RJ0V8Hc88/XHUX/HfiAq7t+BH+x6v8t438enWmdJwFA6ZINriLGKv/95f8lT9/FnyA1NMVEvQyaXuu+gz36f/DD73E4pwqpLcvm/o0Vle78n//+L/NPvoefp1pTJye6e4A/D082FERa5/opeH9zpvh13cNm19/4v/LDe5xMWTi8I0Ta0qKlK27AS/v3/r+/x/2GO9K2c7kVMonDpq7//jc5PKCxeNPpFVzaRr01wF8C4Pu76hXuX18H4LduTr79guuFD3n5BHfI+ZRFhY8w29TYhbbLi/bvBdqKE4fUgg1pBKnV3FEaCWOWyA+m3WpORZr/j+9TKJtW8yBTF2/ZEODI9/QavHkVdGFp/Pjn4Q+u5hXapsP5sOH+OXXA1LiKuqJxiMNbhTkbdJTCy4llEt6NnqRT4dhg1V3nbdrm6dYMecA1yTOL4PWTE9L5VzPFlLBCvlG58AhehnN4uHsAYinyJ+AZ/NkVvELbfOBUuOO5syBIEtiqHU1k9XeISX5bsimrkUUhnGDxourN8SgUsCZVtKyGbyGzHXdjOhsAvOAswSRyIBddRdEZWP6GZhNK/yjwew9ehBo+3jEADu7Ay2n8mDc+TS7awUHg0OMzR0LABhqLD4hJEh/BEGyBdGlSJoXYXtr+3HS4ijzVpgi0paWXtdruGTknXBz+11qT1Q2inxaTzQCO46P3lfLpyS4fou2PH/PupwZgCxNhGlj4IvUuWEsTkqMWm6i4xCSMc9N1RDQoCVcuGItJ/MRWefais+3synowi/dESgJjkilnWnBTGvRWmaw8oR15257t7CHmCf8HOn7cwI8+NQBXMBEmAa8PMRemrNCEhLGEhDQKcGZWS319BX9PFBEwGTbRBhLbDcaV3drFcDqk5kCTd2JF1Wp0HraqBx8U0wwBTnbpCadwBA/gTH/CDrcCs93LV8E0YlmmcyQRQnjBa8JESmGUfIjK/7fkaDJpmD2QptFNVJU1bbtIAjjWQizepOKptRjbzR9Kag6xZmMLLjHOtcLT3Tx9o/0EcTT1XN3E45u24AiwEypDJXihKjQxjLprEwcmRKclaDNZCVqr/V8mYWyFADbusiY5hvgFoU2vio49RgJLn5OsReRFN6tabeetiiy0V7KFHT3HyZLx491u95sn4K1QQSPKM9hNT0wMVvAWbzDSVdrKw4zRjZMyJIHkfq1VAVCDl/bUhNKlGq0zGr05+YAceXVPCttVk0oqjVwMPt+BBefx4yPtGVkUsqY3CHDPiCM5ngupUwCdbkpd8kbPrCWHhkmtIKLEetF2499eS1jZlIPGYnlcPXeM2KD9vLS0bW3ktYNqUllpKLn5ZrsxlIzxvDu5eHxzGLctkZLEY4PgSOg2IUVVcUONzUDBEpRaMoXNmUc0tFZrTZquiLyKxrSm3DvIW9Fil+AkhXu5PhEPx9mUNwqypDvZWdKlhIJQY7vn2OsnmBeOWnYZ0m1iwbbw1U60by5om47iHRV6fOgzjMf/DAZrlP40Z7syxpLK0lJ0gqaAK1c2KQKu7tabTXkLFz0sCftuwX++MyNeNn68k5Buq23YQhUh0SNTJa1ioQ0p4nUG2y0XilF1JqODqdImloPS4Bp111DEWT0jJjVv95uX9BBV7eB3bUWcu0acSVM23YZdd8R8UbQUxJ9wdu3oMuhdt929ME+mh6JXJ8di2RxbTi6TbrDquqV4aUKR2iwT6aZbyOwEXN3DUsWr8Hn4EhwNyHuXHh7/pdaUjtR7vnDh/d8c9xD/s5f501eQ1+CuDiCvGhk1AN/4Tf74RfxPwD3toLarR0zNtsnPzmS64KIRk861dMWCU8ArasG9T9H0ZBpsDGnjtAOM2+/LuIb2iIUGXNgl5ZmKD/Tw8TlaAuihaFP5yrw18v4x1898zIdP+DDAX1bM3GAMvPgRP/cJn3zCW013nrhHkrITyvYuwOUkcHuKlRSW5C6rzIdY4ppnF7J8aAJbQepgbJYBjCY9usGXDKQxq7RZfh9eg5d1UHMVATRaD/4BHK93/1iAgYZ/+jqPn8Dn4UExmWrpa3+ZOK6MvM3bjwfzxNWA2dhs8+51XHSPJiaAhGSpWevEs5xHLXcEGFXYiCONySH3fPWq93JIsBiSWvWyc3CAN+EcXoT7rCSANloPPoa31rt/5PUA/gp8Q/jDD3hyrjzlR8VkanfOvB1XPubt17vzxAfdSVbD1pzAnfgyF3ycadOTOTXhpEUoLC1HZyNGW3dtmjeXgr2r56JNmRwdNNWaQVBddd6rh4MhviEB9EFRD/7RGvePvCbwAL4Mx/D6M541hHO4D3e7g6PafdcZVw689z7NGTwo5om7A8sPhccT6qKcl9NJl9aM/9kX+e59Hh1yPqGuCCZxuITcsmNaJ5F7d0q6J3H48TO1/+M57085q2icdu2U+W36Ldllz9Agiv4YGljoEN908EzvDOrBF98/vtJwCC/BF2AG75xxEmjmMIcjxbjoaxqOK3/4hPOZzhMPBpYPG44CM0dTVm1LjLtUWWVz1Bcf8tEx0zs8O2A2YVHRxKYOiy/aOVoAaMu0i7ubu43njjmd4ibMHU1sIDHaQNKrZND/FZYdk54oCXetjq7E7IVl9eAL7t+oHnwXXtLx44czzoRFHBztYVwtH1d+NOMkupZ5MTM+gUmq90X+Bh9zjRlmaQ+m7YMqUL/veemcecAtOJ0yq1JnVlN27di2E0+Klp1tAJ4KRw1eMI7aJjsO3R8kPSI3fUFXnIOfdQe86sIIVtWDL7h//Ok6vj8vwDk08NEcI8zz7OhBy+WwalzZeZ4+0XniRfst9pAJqQHDGLzVQ2pheZnnv1OWhwO43/AgcvAEXEVVpa4db9sGvNK8wjaENHkfFQ4Ci5i7dqnQlPoLQrHXZDvO3BIXZbJOBrOaEbML6sFL798I4FhKihjHMsPjBUZYCMFr6nvaArxqXPn4lCa+cHfSa2cP27g3Z3ziYTRrcbQNGLQmGF3F3cBdzzzX7AILx0IB9rbwn9kx2G1FW3Inic+ZLIsVvKR8Zwfj0l1fkqo8LWY1M3IX14OX3r9RKTIO+d9XzAI8qRPGPn/4NC2n6o4rN8XJ82TOIvuVA8zLKUHRFgBCetlDZlqR1gLKjS39xoE7Bt8UvA6BxuEDjU3tFsEijgA+615tmZkXKqiEENrh41iLDDZNq4pKTWR3LZfnos81LOuNa15cD956vLMsJd1rqYp51gDUQqMYm2XsxnUhD2jg1DM7SeuJxxgrmpfISSXVIJIS5qJJSvJPEQ49DQTVIbYWJ9QWa/E2+c/oPK1drmC7WSfJRNKBO5Yjvcp7Gc3dmmI/Xh1kDTEuiSnWqQf37h+fTMhGnDf6dsS8SQfQWlqqwXXGlc/PEZ/SC5mtzIV0nAshlQdM/LvUtYutrEZ/Y+EAFtq1k28zQhOwLr1AIeANzhF8t9qzTdZf2qRKO6MWE9ohBYwibbOmrFtNmg3mcS+tB28xv2uKd/agYCvOP+GkSc+0lr7RXzyufL7QbkUpjLjEWFLqOIkAGu2B0tNlO9Eau2W1qcOUvVRgKzypKIQZ5KI3q0MLzqTNRYqiZOqmtqloIRlmkBHVpHmRYV6/HixbO6UC47KOFJnoMrVyr7wYz+SlW6GUaghYbY1I6kkxA2W1fSJokUdSh2LQ1GAimRGm0MT+uu57H5l7QgOWxERpO9moLRPgTtquWCfFlGlIjQaRly9odmzMOWY+IBO5tB4sW/0+VWGUh32qYk79EidWKrjWuiLpiVNGFWFRJVktyeXWmbgBBzVl8anPuXyNJlBJOlKLTgAbi/EYHVHxWiDaVR06GnHQNpJcWcK2jJtiCfG2sEHLzuI66sGrMK47nPIInPnu799935aOK2cvmvubrE38ZzZjrELCmXM2hM7UcpXD2oC3+ECVp7xtIuxptJ0jUr3sBmBS47TVxlvJ1Sqb/E0uLdvLj0lLr29ypdd/eMX3f6lrxGlKwKQxEGvw0qHbkbwrF3uHKwVENbIV2wZ13kNEF6zD+x24aLNMfDTCbDPnEikZFyTNttxWBXDaBuM8KtI2rmaMdUY7cXcUPstqTGvBGSrFWIpNMfbdea990bvAOC1YX0qbc6smDS1mPxSJoW4fwEXvjMmhlijDRq6qale6aJEuFGoppYDoBELQzLBuh/mZNx7jkinv0EtnUp50lO9hbNK57lZaMAWuWR5Yo9/kYwcYI0t4gWM47Umnl3YmpeBPqSyNp3K7s2DSAS/39KRuEN2bS4xvowV3dFRMx/VFcp2Yp8w2nTO9hCXtHG1kF1L4KlrJr2wKfyq77R7MKpFKzWlY9UkhYxyHWW6nBWPaudvEAl3CGcNpSXPZ6R9BbBtIl6cHL3gIBi+42CYXqCx1gfGWe7Ap0h3luyXdt1MKy4YUT9xSF01G16YEdWsouW9mgDHd3veyA97H+Ya47ZmEbqMY72oPztCGvK0onL44AvgC49saZKkWRz4veWljE1FHjbRJaWv6ZKKtl875h4CziFCZhG5rx7tefsl0aRT1bMHZjm8dwL/6u7wCRysaQblQoG5yAQN5zpatMNY/+yf8z+GLcH/Qn0iX2W2oEfXP4GvwQHuIL9AYGnaO3zqAX6946nkgqZNnUhx43DIdQtMFeOPrgy/y3Yd85HlJWwjLFkU3kFwq28xPnuPhMWeS+tDLV9Otllq7pQCf3uXJDN9wFDiUTgefHaiYbdfi3b3u8+iY6TnzhgehI1LTe8lcd7s1wJSzKbahCRxKKztTLXstGAiu3a6rPuQs5pk9TWAan5f0BZmGf7Ylxzzk/A7PAs4QPPPAHeFQ2hbFHszlgZuKZsJcUmbDC40sEU403cEjczstOEypa+YxevL4QBC8oRYqWdK6b7sK25tfE+oDZgtOQ2Jg8T41HGcBE6fTWHn4JtHcu9S7uYgU5KSCkl/mcnq+5/YBXOEr6lCUCwOTOM1taOI8mSxx1NsCXBEmLKbMAg5MkwbLmpBaFOPrNSlO2HnLiEqW3tHEwd8AeiQLmn+2gxjC3k6AxREqvKcJbTEzlpLiw4rNZK6oJdidbMMGX9FULKr0AkW+2qDEPBNNm5QAt2Ik2nftNWHetubosHLo2nG4vQA7GkcVCgVCgaDixHqo9UUn1A6OshapaNR/LPRYFV8siT1cCtJE0k/3WtaNSuUZYKPnsVIW0xXWnMUxq5+En4Kvw/MqQmVXnAXj9Z+9zM98zM/Agy7F/qqj2Nh67b8HjFnPP3iBn/tkpdzwEJX/whIcQUXOaikeliCRGUk7tiwF0rItwMEhjkZ309hikFoRAmLTpEXWuHS6y+am/KB/fM50aLEhGnSMwkpxzOov4H0AvgovwJ1iGzDLtJn/9BU+fAINfwUe6FHSLhu83viV/+/HrOePX+STT2B9uWGbrMHHLldRBlhS/CJQmcRxJFqZica01XixAZsYiH1uolZxLrR/SgxVIJjkpQP4PE9sE59LKLr7kltSBogS5tyszzH8Fvw8/AS8rNOg0xUS9fIaHwb+6et8Q/gyvKRjf5OusOzGx8evA/BP4IP11uN/grca5O0lcsPLJ5YjwI4QkJBOHa0WdMZYGxPbh2W2nR9v3WxEWqgp/G3+6VZbRLSAAZ3BhdhAaUL33VUSw9yjEsvbaQ9u4A/gGXwZXoEHOuU1GSj2chf+Mo+f8IcfcAxfIKVmyunRbYQVnoevwgfw3TXXcw++xNuP4fhyueEUNttEduRVaDttddoP0eSxLe2LENk6itYxlrxBNBYrNNKSQmeaLcm9c8UsaB5WyO6675yyQIAWSDpBVoA/gxmcwEvwoDv0m58UE7gHn+fJOa8/Ywan8EKRfjsopF83eCglX/Sfr7OeaRoQfvt1CGvIDccH5BCvw1sWIzRGC/66t0VTcLZQZtm6PlAasbOJ9iwWtUo7biktTSIPxnR24jxP1ZKaqq+2RcXM9OrBAm/AAs7hDJ5bNmGb+KIfwCs8a3jnjBrOFeMjHSCdbKr+2uOLfnOd9eiA8Hvvwwq54VbP2OqwkB48Ytc4YEOiH2vTXqodabfWEOzso4qxdbqD5L6tbtNPECqbhnA708DZH4QOJUXqScmUlks7Ot6FBuZw3n2mEbaUX7kDzxHOOQk8nKWMzAzu6ZZ8sOFw4RK+6PcuXo9tB4SbMz58ApfKDXf3szjNIIbGpD5TKTRxGkEMLjLl+K3wlWXBsCUxIDU+jbOiysESqAy1MGUJpXgwbTWzNOVEziIXZrJ+VIztl1PUBxTSo0dwn2bOmfDRPD3TRTGlfbCJvO9KvuhL1hMHhB9wPuPRLGHcdOWG2xc0U+5bQtAJT0nRTewXL1pgk2+rZAdeWmz3jxAqfNQQdzTlbF8uJ5ecEIWvTkevAHpwz7w78QujlD/Lr491bD8/1vhM2yrUQRrWXNQY4fGilfctMWYjL72UL/qS9eiA8EmN88nbNdour+PBbbAjOjIa4iBhfFg6rxeKdEGcL6p3EWR1Qq2Qkhs2DrnkRnmN9tG2EAqmgPw6hoL7Oza7B+3SCrR9tRftko+Lsf2F/mkTndN2LmzuMcKTuj/mX2+4Va3ki16+nnJY+S7MefpkidxwnV+4wkXH8TKnX0tsYzYp29DOOoSW1nf7nTh2akYiWmcJOuTidSaqESrTYpwjJJNVGQr+rLI7WsqerHW6Kp/oM2pKuV7T1QY9gjqlZp41/WfKpl56FV/0kvXQFRyeQ83xaTu5E8p5dNP3dUF34ihyI3GSpeCsywSh22ZJdWto9winhqifb7VRvgktxp13vyjrS0EjvrRfZ62uyqddSWaWYlwTPAtJZ2oZ3j/Sgi/mi+6vpzesfAcWNA0n8xVyw90GVFGuZjTXEQy+6GfLGLMLL523f5E0OmxVjDoOuRiH91RKU+vtoCtH7TgmvBLvtFXWLW15H9GTdVw8ow4IlRLeHECN9ym1e9K0I+Cbnhgv4Yu+aD2HaQJ80XDqOzSGAV4+4yCqBxrsJAX6ZTIoX36QnvzhhzzMfFW2dZVLOJfo0zbce5OvwXMFaZ81mOnlTVXpDZsQNuoYWveketKb5+6JOOsgX+NTm7H49fUTlx+WLuWL7qxnOFh4BxpmJx0p2gDzA/BUARuS6phR+pUsY7MMboAHx5xNsSVfVZcYSwqCKrqon7zM+8ecCkeS4nm3rINuaWvVNnMRI1IRpxTqx8PZUZ0Br/UEduo3B3hNvmgZfs9gQPj8vIOxd2kndir3awvJ6BLvoUuOfFWNYB0LR1OQJoUySKb9IlOBx74q1+ADC2G6rOdmFdJcD8BkfualA+BdjOOzP9uUhGUEX/TwhZsUduwRr8wNuXKurCixLBgpQI0mDbJr9dIqUuV+92ngkJZ7xduCk2yZKbfWrH1VBiTg9VdzsgRjW3CVXCvAwDd+c1z9dWw9+B+8MJL/eY15ZQ/HqvTwVdsZn5WQsgRRnMaWaecu3jFvMBEmgg+FJFZsnSl0zjB9OqPYaBD7qmoVyImFvzi41usesV0julaAR9dfR15Xzv9sEruRDyk1nb+QaLU67T885GTls6YgcY+UiMa25M/pwGrbCfzkvR3e0jjtuaFtnwuagHTSb5y7boBH119HXhvwP487jJLsLJ4XnUkHX5sLbS61dpiAXRoZSCrFJ+EjpeU3puVfitngYNo6PJrAigKktmwjyQdZpfq30mmtulaAx9Zfx15Xzv+cyeuiBFUs9zq8Kq+XB9a4PVvph3GV4E3y8HENJrN55H1X2p8VyqSKwVusJDKzXOZzplWdzBUFK9e+B4+uv468xvI/b5xtSAkBHQaPvtqWzllVvEOxPbuiE6+j2pvjcKsbvI7txnRErgfH7LdXqjq0IokKzga14GzQ23SSbCQvO6r+Or7SMIr/efOkkqSdMnj9mBx2DRsiY29Uj6+qK9ZrssCKaptR6HKURdwUYeUWA2kPzVKQO8ku2nU3Anhs/XWkBx3F/7wJtCTTTIKftthue1ty9xvNYLY/zo5KSbIuKbXpbEdSyeRyYdAIwKY2neyoc3+k1XUaufYga3T9daMUx/r8z1s10ITknIO0kuoMt+TB8jK0lpayqqjsJ2qtXAYwBU932zinimgmd6mTRDnQfr88q36NAI+tv24E8Pr8zxtasBqx0+xHH9HhlrwsxxNUfKOHQaZBITNf0uccj8GXiVmXAuPEAKSdN/4GLHhs/XWj92dN/uetNuBMnVR+XWDc25JLjo5Mg5IZIq226tmCsip2zZliL213YrTlL2hcFjpCduyim3M7/eB16q/blQsv5X/esDRbtJeabLIosWy3ycavwLhtxdWzbMmHiBTiVjJo6lCLjXZsi7p9PEPnsq6X6wd4bP11i0rD5fzPm/0A6brrIsllenZs0lCJlU4abakR59enZKrKe3BZihbTxlyZ2zl1+g0wvgmA166/bhwDrcn/7Ddz0eWZuJvfSESug6NzZsox3Z04FIxz0mUjMwVOOVTq1CQ0AhdbBGVdjG/CgsfUX7esJl3K/7ytWHRv683praW/8iDOCqWLLhpljDY1ZpzK75QiaZoOTpLKl60auHS/97oBXrv+umU9+FL+5+NtLFgjqVLCdbmj7pY5zPCPLOHNCwXGOcLquOhi8CmCWvbcuO73XmMUPab+ug3A6/A/78Bwe0bcS2+tgHn4J5pyS2WbOck0F51Vq3LcjhLvZ67p1ABbaL2H67bg78BfjKi/jr3+T/ABV3ilLmNXTI2SpvxWBtt6/Z//D0z/FXaGbSBgylzlsEGp+5//xrd4/ae4d8DUUjlslfIYS3t06HZpvfQtvv0N7AHWqtjP2pW08QD/FLy//da38vo8PNlKHf5y37Dxdfe/oj4kVIgFq3koLReSR76W/bx//n9k8jonZxzWTANVwEniDsg87sOSd/z7//PvMp3jQiptGVWFX2caezzAXwfgtzYUvbr0iozs32c3Uge7varH+CNE6cvEYmzbPZ9hMaYDdjK4V2iecf6EcEbdUDVUARda2KzO/JtCuDbNQB/iTeL0EG1JSO1jbXS+nLxtPMDPw1fh5+EPrgSEKE/8Gry5A73ui87AmxwdatyMEBCPNOCSKUeRZ2P6Myb5MRvgCHmA9ywsMifU+AYXcB6Xa5GibUC5TSyerxyh0j6QgLVpdyhfArRTTLqQjwe4HOD9s92D4Ap54odXAPBWLAwB02igG5Kkc+piN4lvODIFGAZgT+EO4Si1s7fjSR7vcQETUkRm9O+MXyo9OYhfe4xt9STQ2pcZRLayCV90b4D3jR0DYAfyxJ+eywg2IL7NTMXna7S/RpQ63JhWEM8U41ZyQGjwsVS0QBrEKLu8xwZsbi4wLcCT+OGidPIOCe1PiSc9Qt+go+vYqB7cG+B9d8cAD+WJPz0Am2gxXgU9IneOqDpAAXOsOltVuMzpdakJXrdPCzXiNVUpCeOos5cxnpQT39G+XVLhs1osQVvJKPZyNq8HDwd4d7pNDuWJPxVX7MSzqUDU6gfadKiNlUFTzLeFHHDlzO4kpa7aiKhBPGKwOqxsBAmYkOIpipyXcQSPlRTf+Tii0U3EJGaZsDER2qoB3h2hu0qe+NNwUooYU8y5mILbJe6OuX+2FTKy7bieTDAemaQyQ0CPthljSWO+xmFDIYiESjM5xKd6Ik5lvLq5GrQ3aCMLvmCA9wowLuWJb9xF59hVVP6O0CrBi3ZjZSNOvRy+I6klNVRJYRBaEzdN+imiUXQ8iVF8fsp+W4JXw7WISW7fDh7lptWkCwZ4d7QTXyBPfJMYK7SijjFppGnlIVJBJBYj7eUwtiP1IBXGI1XCsjNpbjENVpSAJ2hq2LTywEly3hUYazt31J8w2+aiLx3g3fohXixPfOMYm6zCGs9LVo9MoW3MCJE7R5u/WsOIjrqBoHUO0bJE9vxBpbhsd3+Nb4/vtPCZ4oZYCitNeYuC/8UDvDvy0qvkiW/cgqNqRyzqSZa/s0mqNGjtKOoTm14zZpUauiQgVfqtQiZjq7Q27JNaSK5ExRcrGCXO1FJYh6jR6CFqK7bZdQZ4t8g0rSlPfP1RdBtqaa9diqtzJkQ9duSryi2brQXbxDwbRUpFMBHjRj8+Nt7GDKgvph9okW7LX47gu0SpGnnFQ1S1lYldOsC7hYteR574ZuKs7Ei1lBsfdz7IZoxzzCVmmVqaSySzQbBVAWDek+N4jh9E/4VqZrJjPwiv9BC1XcvOWgO8275CVyBPvAtTVlDJfZkaZGU7NpqBogAj/xEHkeAuJihWYCxGN6e8+9JtSegFXF1TrhhLGP1fak3pebgPz192/8gB4d/6WT7+GdYnpH7hH/DJzzFiYPn/vjW0SgNpTNuPIZoAEZv8tlGw4+RLxy+ZjnKa5NdFoC7UaW0aduoYse6+bXg1DLg6UfRYwmhGEjqPvF75U558SANrElK/+MdpXvmqBpaXOa/MTZaa1DOcSiLaw9j0NNNst3c+63c7EKTpkvKHzu6bPbP0RkuHAVcbRY8ijP46MIbQeeT1mhA+5PV/inyDdQipf8LTvMXbwvoDy7IruDNVZKTfV4CTSRUYdybUCnGU7KUTDxLgCknqUm5aAW6/1p6eMsOYsphLzsHrE0Y/P5bQedx1F/4yPHnMB3/IOoTU9+BL8PhtjuFKBpZXnYNJxTuv+2XqolKR2UQgHhS5novuxVySJhBNRF3SoKK1XZbbXjVwWNyOjlqWJjrWJIy+P5bQedyldNScP+HZ61xKSK3jyrz+NiHG1hcOLL/+P+PDF2gOkekKGiNWKgJ+8Z/x8Iv4DdQHzcpZyF4v19I27w9/yPGDFQvmEpKtqv/TLiWMfn4sofMm9eAH8Ao0zzh7h4sJqYtxZd5/D7hkYPneDzl5idlzNHcIB0jVlQ+8ULzw/nc5/ojzl2juE0apD7LRnJxe04dMz2iOCFNtGFpTuXA5AhcTRo8mdN4kz30nVjEC4YTZQy4gpC7GlTlrePKhGsKKgeXpCYeO0MAd/GH7yKQUlXPLOasOH3FnSphjHuDvEu4gB8g66oNbtr6eMbFIA4fIBJkgayoXriw2XEDQPJrQeROAlY6aeYOcMf+IVYTU3XFlZufMHinGywaW3YLpObVBAsbjF4QJMsVUSayjk4voPsHJOQfPWDhCgDnmDl6XIRerD24HsGtw86RMHOLvVSHrKBdeVE26gKB5NKHzaIwLOmrqBWJYZDLhASG16c0Tn+CdRhWDgWXnqRZUTnPIHuMJTfLVpkoYy5CzylHVTGZMTwkGAo2HBlkQplrJX6U+uF1wZz2uwS1SQ12IqWaPuO4baZaEFBdukksJmkcTOm+YJSvoqPFzxFA/YUhIvWxcmSdPWTWwbAKVp6rxTtPFUZfKIwpzm4IoMfaYQLWgmlG5FME2gdBgm+J7J+rtS/XBbaVLsR7bpPQnpMFlo2doWaVceHk9+MkyguZNCJ1He+kuHTWyQAzNM5YSUg/GlTk9ZunAsg1qELVOhUSAK0LABIJHLKbqaEbHZLL1VA3VgqoiOKXYiS+HRyaEKgsfIqX64HYWbLRXy/qWoylIV9gudL1OWBNgBgTNmxA6b4txDT4gi3Ri7xFSLxtXpmmYnzAcWDZgY8d503LFogz5sbonDgkKcxGsWsE1OI+rcQtlgBBCSOKD1mtqYpIU8cTvBmAT0yZe+zUzeY92fYjTtGipXLhuR0ePoHk0ofNWBX+lo8Z7pAZDk8mEw5L7dVyZZoE/pTewbI6SNbiAL5xeygW4xPRuLCGbhcO4RIeTMFYHEJkYyEO9HmJfXMDEj/LaH781wHHZEtqSQ/69UnGpzH7LKIAZEDSPJnTesJTUa+rwTepI9dLJEawYV+ZkRn9g+QirD8vF8Mq0jFQ29js6kCS3E1+jZIhgPNanHdHFqFvPJLHqFwQqbIA4jhDxcNsOCCQLDomaL/dr5lyJaJU6FxPFjO3JOh3kVMcROo8u+C+jo05GjMF3P3/FuDLn5x2M04xXULPwaS6hBYki+MrMdZJSgPHlcB7nCR5bJ9Kr5ACUn9jk5kivdd8tk95SOGrtqu9lr2IhK65ZtEl7ZKrp7DrqwZfRUSN1el7+7NJxZbywOC8neNKTch5vsTEMNsoCCqHBCqIPRjIPkm0BjvFODGtto99rCl+d3wmHkW0FPdpZtC7MMcVtGFQjJLX5bdQ2+x9ypdc313uj8xlsrfuLgWXz1cRhZvJYX0iNVBRcVcmCXZs6aEf3RQF2WI/TcCbKmGU3IOoDJGDdDub0+hYckt6PlGu2BcxmhbTdj/klhccLGJMcqRjMJP1jW2ETqLSWJ/29MAoORluJ+6LPffBZbi5gqi5h6catQpmOT7/OFf5UorRpLzCqcMltBLhwd1are3kztrSzXO0LUbXRQcdLh/RdSZ+swRm819REDrtqzC4es6Gw4JCKlSnjYVpo0xeq33PrADbFLL3RuCmObVmPN+24kfa+AojDuM4umKe2QwCf6EN906HwjujaitDs5o0s1y+k3lgbT2W2i7FJdnwbLXhJUBq/9liTctSmFC/0OqUinb0QddTWamtjbHRFuWJJ6NpqZ8vO3fZJ37Db+2GkaPYLGHs7XTTdiFQJ68SkVJFVmY6McR5UycflNCsccHFaV9FNbR4NttLxw4pQ7wJd066Z0ohVbzihaxHVExd/ay04oxUKWt+AsdiQ9OUyZ2krzN19IZIwafSTFgIBnMV73ADj7V/K8u1MaY2sJp2HWm0f41tqwajEvdHWOJs510MaAqN4aoSiPCXtN2KSi46dUxHdaMquar82O1x5jqhDGvqmoE9LfxcY3zqA7/x3HA67r9ZG4O6Cuxu12/+TP+eLP+I+HErqDDCDVmBDO4larujNe7x8om2rMug0MX0rL1+IWwdwfR+p1TNTyNmVJ85ljWzbWuGv8/C7HD/izjkHNZNYlhZcUOKVzKFUxsxxN/kax+8zPWPSFKw80rJr9Tizyj3o1gEsdwgWGoxPezDdZ1TSENE1dLdNvuKL+I84nxKesZgxXVA1VA1OcL49dFlpFV5yJMhzyCmNQ+a4BqusPJ2bB+xo8V9u3x48VVIEPS/mc3DvAbXyoYr6VgDfh5do5hhHOCXMqBZUPhWYbWZECwVJljLgMUWOCB4MUuMaxGNUQDVI50TQ+S3kFgIcu2qKkNSHVoM0SHsgoZxP2d5HH8B9woOk4x5bPkKtAHucZsdykjxuIpbUrSILgrT8G7G5oCW+K0990o7E3T6AdW4TilH5kDjds+H64kS0mz24grtwlzDHBJqI8YJQExotPvoC4JBq0lEjjQkyBZ8oH2LnRsQ4Hu1QsgDTJbO8fQDnllitkxuVskoiKbRF9VwzMDvxHAdwB7mD9yCplhHFEyUWHx3WtwCbSMMTCUCcEmSGlg4gTXkHpZXWQ7kpznK3EmCHiXInqndkQjunG5kxTKEeGye7jWz9cyMR2mGiFQ15ENRBTbCp+Gh86vAyASdgmJq2MC6hoADQ3GosP0QHbnMHjyBQvQqfhy/BUbeHd5WY/G/9LK/8Ka8Jd7UFeNWEZvzPb458Dn8DGLOe3/wGL/4xP+HXlRt+M1PE2iLhR8t+lfgxsuh7AfO2AOf+owWhSZRYQbd622hbpKWKuU+XuvNzP0OseRDa+mObgDHJUSc/pKx31QdKffQ5OIJpt8GWjlgTwMc/w5MPCR/yl1XC2a2Yut54SvOtMev55Of45BOat9aWG27p2ZVORRvnEk1hqWMVUmqa7S2YtvlIpspuF1pt0syuZS2NV14mUidCSfzQzg+KqvIYCMljIx2YK2AO34fX4GWdu5xcIAb8MzTw+j/lyWM+Dw/gjs4GD6ehNgA48kX/AI7XXM/XAN4WHr+9ntywqoCakCqmKP0rmQrJJEErG2Upg1JObr01lKQy4jskWalKYfJ/EDLMpjNSHFEUAde2fltaDgmrNaWQ9+AAb8I5vKjz3L1n1LriB/BXkG/wwR9y/oRX4LlioHA4LzP2inzRx/DWmutRweFjeP3tNeSGlaE1Fde0OS11yOpmbIp2u/jF1n2RRZviJM0yBT3IZl2HWImKjQOxIyeU325b/qWyU9Moj1o07tS0G7qJDoGHg5m8yeCxMoEH8GU45tnrNM84D2l297DQ9t1YP7jki/7RmutRweEA77/HWXOh3HCxkRgldDQkAjNTMl2Iloc1qN5JfJeeTlyTRzxURTdn1Ixv2uKjs12AbdEWlBtmVdk2k7FFwj07PCZ9XAwW3dG+8xKzNFr4EnwBZpy9Qzhh3jDXebBpYcpuo4fQ44u+fD1dweEnHzI7v0xuuOALRUV8rXpFyfSTQYkhd7IHm07jpyhlkCmI0ALYqPTpUxXS+z4jgDj1Pflvmz5ecuItpIBxyTHpSTGWd9g1ApfD/bvwUhL4nT1EzqgX7cxfCcNmb3mPL/qi9SwTHJ49oj5ZLjccbTG3pRmlYi6JCG0mQrAt1+i2UXTZ2dv9IlQpN5naMYtviaXlTrFpoMsl3bOAFEa8sqPj2WCMrx3Yjx99qFwO59Aw/wgx+HlqNz8oZvA3exRDvuhL1jMQHPaOJ0+XyA3fp1OfM3qObEVdhxjvynxNMXQV4+GJyvOEFqeQBaIbbO7i63rpxCltdZShPFxkjM2FPVkn3TG+Rp9pO3l2RzFegGfxGDHIAh8SteR0C4HopXzRF61nheDw6TFN05Ebvq8M3VKKpGjjO6r7nhudTEGMtYM92HTDaR1FDMXJ1eThsbKfywyoWwrzRSXkc51flG3vIid62h29bIcFbTGhfV+faaB+ohj7dPN0C2e2lC96+XouFByen9AsunLDJZ9z7NExiUc0OuoYW6UZkIyx2YUR2z6/TiRjyKMx5GbbjLHvHuf7YmtKghf34LJfx63Yg8vrvN2zC7lY0x0tvKezo4HmGYDU+Gab6dFL+KI761lDcNifcjLrrr9LWZJctG1FfU1uwhoQE22ObjdfkSzY63CbU5hzs21WeTddH2BaL11Gi7lVdlxP1nkxqhnKhVY6knS3EPgVGg1JpN5cP/hivujOelhXcPj8HC/LyI6MkteVjlolBdMmF3a3DbsuAYhL44dxzthWSN065xxUd55Lmf0wRbOYOqH09/o9WbO2VtFdaMb4qBgtFJoT1SqoN8wPXMoXLb3p1PUEhxfnnLzGzBI0Ku7FxrKsNJj/8bn/H8fPIVOd3rfrklUB/DOeO+nkghgSPzrlPxluCMtOnDL4Yml6dK1r3vsgMxgtPOrMFUZbEUbTdIzii5beq72G4PD0DKnwjmBULUVFmy8t+k7fZ3pKc0Q4UC6jpVRqS9Umv8bxw35flZVOU1X7qkjnhZlsMbk24qQ6Hz7QcuL6sDC0iHHki96Uh2UdvmgZnjIvExy2TeJdMDZNSbdZyAHe/Yd1xsQhHiKzjh7GxQ4yqMPaywPkjMamvqrYpmO7Knad+ZQC5msCuAPWUoxrxVhrGv7a+KLXFhyONdTMrZ7ke23qiO40ZJUyzgYyX5XyL0mV7NiUzEs9mjtbMN0dERqwyAJpigad0B3/zRV7s4PIfXSu6YV/MK7+OrYe/JvfGMn/PHJe2fyUdtnFrKRNpXV0Y2559aWPt/G4BlvjTMtXlVIWCnNyA3YQBDmYIodFz41PvXPSa6rq9lWZawZ4dP115HXV/M/tnFkkrBOdzg6aP4pID+MZnTJ1SuuB6iZlyiox4HT2y3YBtkUKWooacBQUDTpjwaDt5poBHl1/HXltwP887lKKXxNUEyPqpGTyA699UqY/lt9yGdlUKra0fFWS+36iylVWrAyd7Uw0CZM0z7xKTOduznLIjG2Hx8cDPLb+OvK6Bv7n1DYci4CxUuRxrjBc0bb4vD3rN5Zz36ntLb83eVJIB8LiIzCmn6SMPjlX+yNlTjvIGjs+QzHPf60Aj62/jrzG8j9vYMFtm1VoRWCJdmw7z9N0t+c8cxZpPeK4aTRicS25QhrVtUp7U578chk4q04Wx4YoQSjFryUlpcQ1AbxZ/XVMknIU//OGl7Q6z9Zpxi0+3yFhSkjUDpnCIUhLWVX23KQ+L9vKvFKI0ZWFQgkDLvBoylrHNVmaw10zwCPrr5tlodfnf94EWnQ0lFRWy8pW9LbkLsyUVDc2NSTHGDtnD1uMtchjbCeb1mpxFP0YbcClhzdLu6lfO8Bj6q+bdT2sz/+8SZCV7VIxtt0DUn9L7r4cLYWDSXnseEpOGFuty0qbOVlS7NNzs5FOGJUqQpl2Q64/yBpZf90sxbE+//PGdZ02HSipCbmD6NItmQ4Lk5XUrGpDMkhbMm2ZVheNYV+VbUWTcv99+2NyX1VoafSuC+AN6q9bFIMv5X/eagNWXZxEa9JjlMwNWb00akGUkSoepp1/yRuuqHGbUn3UdBSTxBU6SEVklzWRUkPndVvw2PrrpjvxOvzPmwHc0hpmq82npi7GRro8dXp0KXnUQmhZbRL7NEVp1uuZmO45vuzKsHrktS3GLWXODVjw+vXXLYx4Hf7njRPd0i3aoAGX6W29GnaV5YdyDj9TFkakje7GHYzDoObfddHtOSpoi2SmzJHrB3hM/XUDDEbxP2/oosszcRlehWXUvzHv4TpBVktHqwenFo8uLVmy4DKLa5d3RtLrmrM3aMFr1183E4sewf+85VWeg1c5ag276NZrM9IJVNcmLEvDNaV62aq+14IAOGFsBt973Ra8Xv11YzXwNfmft7Jg2oS+XOyoC8/cwzi66Dhmgk38kUmP1CUiYWOX1bpD2zWXt2FCp7uq8703APAa9dfNdscR/M/bZLIyouVxqJfeWvG9Je+JVckHQ9+CI9NWxz+blX/KYYvO5n2tAP/vrlZ7+8/h9y+9qeB/Hnt967e5mevX10rALDWK//FaAT5MXdBXdP0C/BAes792c40H+AiAp1e1oH8HgH94g/Lttx1gp63op1eyoM/Bvw5/G/7xFbqJPcCXnmBiwDPb/YKO4FX4OjyCb289db2/Noqicw4i7N6TVtoz8tNwDH+8x/i6Ae7lmaQVENzJFb3Di/BFeAwz+Is9SjeQySpPqbLFlNmyz47z5a/AF+AYFvDmHqibSXTEzoT4Gc3OALaqAP4KPFUJ6n+1x+rGAM6Zd78bgJ0a8QN4GU614vxwD9e1Amy6CcskNrczLx1JIp6HE5UZD/DBHrFr2oNlgG4Odv226BodoryjGJ9q2T/AR3vQrsOCS0ctXZi3ruLlhpFDJYl4HmYtjQCP9rhdn4suySLKDt6wLcC52h8xPlcjju1fn+yhuw4LZsAGUuo2b4Fx2UwQu77uqRHXGtg92aN3tQCbFexc0uk93vhTXbct6y7MulLycoUljx8ngDMBg1tvJjAazpEmOtxlzclvj1vQf1Tx7QlPDpGpqgtdSKz/d9/hdy1vTfFHSmC9dGDZbLiezz7Ac801HirGZsWjydfZyPvHXL/Y8Mjzg8BxTZiuwKz4Eb8sBE9zznszmjvFwHKPIWUnwhqfVRcd4Ck0K6ate48m1oOfrX3/yOtvAsJ8zsPAM89sjnddmuLuDPjX9Bu/L7x7xpMzFk6nWtyQfPg278Gn4Aekz2ZgOmU9eJ37R14vwE/BL8G3aibCiWMWWDQ0ZtkPMnlcGeAu/Ag+8ZyecU5BPuy2ILD+sQqyZhAKmn7XZd+jIMTN9eBL7x95xVLSX4On8EcNlXDqmBlqS13jG4LpmGbkF/0CnOi3H8ETOIXzmnmtb0a16Tzxj1sUvQCBiXZGDtmB3KAefPH94xcUa/6vwRn80GOFyjEXFpba4A1e8KQfFF+259tx5XS4egYn8fQsLGrqGrHbztr+uByTahWuL1NUGbDpsnrwBfePPwHHIf9X4RnM4Z2ABWdxUBlqQ2PwhuDxoS0vvqB1JzS0P4h2nA/QgTrsJFn+Y3AOjs9JFC07CGWX1oNX3T/yHOzgDjwPn1PM3g9Jk9lZrMEpxnlPmBbjyo2+KFXRU52TJM/2ALcY57RUzjObbjqxVw++4P6RAOf58pcVsw9Daje3htriYrpDOonre3CudSe6bfkTEgHBHuDiyu5MCsc7BHhYDx7ePxLjqigXZsw+ijMHFhuwBmtoTPtOxOrTvYJDnC75dnUbhfwu/ZW9AgYd+peL68HD+0emKquiXHhWjJg/UrkJYzuiaL3E9aI/ytrCvAd4GcYZMCkSQxfUg3v3j8c4e90j5ZTPdvmJJGHnOCI2nHS8081X013pHuBlV1gB2MX1YNmWLHqqGN/TWmG0y6clJWthxNUl48q38Bi8vtMKyzzpFdSDhxZ5WBA5ZLt8Jv3895DduBlgbPYAj8C4B8hO68FDkoh5lydC4FiWvBOVqjYdqjiLv92t8yPDjrDaiHdUD15qkSURSGmXJwOMSxWAXYwr3zaAufJ66l+94vv3AO+vPcD7aw/w/toDvL/2AO+vPcD7aw/wHuD9tQd4f+0B3l97gPfXHuD9tQd4f+0B3l97gG8LwP8G/AL8O/A5OCq0Ys2KIdv/qOIXG/4mvFAMF16gZD+2Xvu/B8as5+8bfllWyg0zaNO5bfXj6vfhhwD86/Aq3NfRS9t9WPnhfnvCIw/CT8GLcFTMnpntdF/z9V+PWc/vWoIH+FL3Znv57PitcdGP4R/C34avw5fgRVUInCwbsn1yyA8C8zm/BH8NXoXnVE6wVPjdeCI38kX/3+Ct9dbz1pTmHFRu+Hm4O9Ch3clr99negxfwj+ER/DR8EV6B5+DuQOnTgUw5rnkY+FbNU3gNXh0o/JYTuWOvyBf9FvzX663HH/HejO8LwAl8Hl5YLTd8q7sqA3wbjuExfAFegQdwfyDoSkWY8swzEf6o4Qyewefg+cHNbqMQruSL/u/WWc+E5g7vnnEXgDmcDeSGb/F4cBcCgT+GGRzDU3hZYburAt9TEtHgbM6JoxJ+6NMzzTcf6c2bycv2+KK/f+l6LBzw5IwfqZJhA3M472pWT/ajKxnjv4AFnMEpnBTPND6s2J7qHbPAqcMK74T2mZ4VGB9uJA465It+/eL1WKhYOD7xHOkr1ajK7d0C4+ke4Hy9qXZwpgLr+Znm/uNFw8xQOSy8H9IzjUrd9+BIfenYaylf9FsXr8fBAadnPIEDna8IBcwlxnuA0/Wv6GAWPd7dDIKjMdSWueAsBj4M7TOd06qBbwDwKr7oleuxMOEcTuEZTHWvDYUO7aHqAe0Bbq+HEFRzOz7WVoTDQkVds7A4sIIxfCQdCefFRoIOF/NFL1mPab/nvOakSL/Q1aFtNpUb/nFOVX6gzyg/1nISyDfUhsokIzaBR9Kxm80s5mK+6P56il1jXic7nhQxsxSm3OwBHl4fFdLqi64nDQZvqE2at7cWAp/IVvrN6/BFL1mPhYrGMBfOi4PyjuSGf6wBBh7p/FZTghCNWGgMzlBbrNJoPJX2mW5mwZfyRffXo7OFi5pZcS4qZUrlViptrXtw+GQoyhDPS+ANjcGBNRiLCQDPZPMHuiZfdFpPSTcQwwKYdRNqpkjm7AFeeT0pJzALgo7g8YYGrMHS0iocy+YTm2vyRUvvpXCIpQ5pe666TJrcygnScUf/p0NDs/iAI/nqDHC8TmQT8x3NF91l76oDdQGwu61Z6E0ABv7uO1dbf/37Zlv+Zw/Pbh8f1s4Avur6657/+YYBvur6657/+YYBvur6657/+YYBvur6657/+aYBvuL6657/+VMA8FXWX/f8zzcN8BXXX/f8zzcNMFdbf93zP38KLPiK6697/uebtuArrr/u+Z9vGmCusP6653/+1FjwVdZf9/zPN7oHX339dc//fNMu+irrr3v+50+Bi+Zq6697/uebA/jz8Pudf9ht/fWv517J/XUzAP8C/BAeX9WCDrUpZ3/dEMBxgPcfbtTVvsYV5Yn32u03B3Ac4P3b8I+vxNBKeeL9dRMAlwO83959qGO78sT769oB7g3w/vGVYFzKE++v6wV4OMD7F7tckFkmT7y/rhHgpQO8b+4Y46XyxPvrugBeNcB7BRiX8sT767oAvmCA9woAHsoT76+rBJjLBnh3txOvkifeX1dswZcO8G6N7sXyxPvr6i340gHe3TnqVfLE++uKAb50gHcXLnrX8sR7gNdPRqwzwLu7Y/FO5Yn3AK9jXCMGeHdgxDuVJ75VAI8ljP7PAb3/RfjcZfePHBB+79dpfpH1CanN30d+mT1h9GqAxxJGM5LQeeQ1+Tb+EQJrElLb38VHQ94TRq900aMIo8cSOo+8Dp8QfsB8zpqE1NO3OI9Zrj1h9EV78PqE0WMJnUdeU6E+Jjyk/hbrEFIfeWbvId8H9oTRFwdZaxJGvziW0Hn0gqYB/wyZ0PwRlxJST+BOw9m77Amj14ii1yGM/txYQudN0qDzGe4EqfA/5GJCagsHcPaEPWH0esekSwmjRxM6b5JEcZ4ww50ilvAOFxBSx4yLW+A/YU8YvfY5+ALC6NGEzhtmyZoFZoarwBLeZxUhtY4rc3bKnjB6TKJjFUHzJoTOozF2YBpsjcyxDgzhQ1YRUse8+J4wenwmaylB82hC5w0zoRXUNXaRBmSMQUqiWSWkLsaVqc/ZE0aPTFUuJWgeTei8SfLZQeMxNaZSIzbII4aE1Nmr13P2hNHjc9E9guYNCZ032YlNwESMLcZiLQHkE4aE1BFg0yAR4z1h9AiAGRA0jyZ03tyIxWMajMPWBIsxYJCnlITU5ShiHYdZ94TR4wCmSxg9jtB5KyPGYzymAYexWEMwAPIsAdYdV6aObmNPGD0aYLoEzaMJnTc0Ygs+YDw0GAtqxBjkuP38bMRWCHn73xNGjz75P73WenCEJnhwyVe3AEe8TtKdJcYhBl97wuhNAObK66lvD/9J9NS75v17wuitAN5fe4D31x7g/bUHeH/tAd5fe4D3AO+vPcD7aw/w/toDvL/2AO+vPcD7aw/w/toDvAd4f/24ABzZ8o+KLsSLS+Pv/TqTb3P4hKlQrTGh+fbIBT0Axqznnb+L/V2mb3HkN5Mb/nEHeK7d4IcDld6lmDW/iH9E+AH1MdOw/Jlu2T1xNmY98sv4wHnD7D3uNHu54WUuOsBTbQuvBsPT/UfzNxGYzwkP8c+Yz3C+r/i6DcyRL/rZ+utRwWH5PmfvcvYEt9jLDS/bg0/B64DWKrQM8AL8FPwS9beQCe6EMKNZYJol37jBMy35otdaz0Bw2H/C2Smc7+WGB0HWDELBmOByA3r5QONo4V+DpzR/hFS4U8wMW1PXNB4TOqYz9urxRV++ntWCw/U59Ty9ebdWbrgfRS9AYKKN63ZokZVygr8GZ/gfIhZXIXPsAlNjPOLBby5c1eOLvmQ9lwkOy5x6QV1j5TYqpS05JtUgUHUp5toHGsVfn4NX4RnMCe+AxTpwmApTYxqMxwfCeJGjpXzRF61nbcHhUBPqWze9svwcHJ+S6NPscKrEjug78Dx8Lj3T8D4YxGIdxmJcwhi34fzZUr7olevZCw5vkOhoClq5zBPZAnygD/Tl9EzDh6kl3VhsHYcDEb+hCtJSvuiV69kLDm+WycrOTArHmB5/VYyP6jOVjwgGawk2zQOaTcc1L+aLXrKeveDwZqlKrw8U9Y1p66uK8dEzdYwBeUQAY7DbyYNezBfdWQ97weEtAKYQg2xJIkuveAT3dYeLGH+ShrWNwZgN0b2YL7qznr3g8JYAo5bQBziPjx7BPZ0d9RCQp4UZbnFdzBddor4XHN4KYMrB2qHFRIzzcLAHQZ5the5ovui94PCWAPefaYnxIdzRwdHCbuR4B+tbiy96Lzi8E4D7z7S0mEPd+eqO3cT53Z0Y8SV80XvB4Z0ADJi/f7X113f+7p7/+UYBvur6657/+YYBvur6657/+aYBvuL6657/+aYBvuL6657/+aYBvuL6657/+aYBvuL6657/+VMA8FXWX/f8z58OgK+y/rrnf75RgLna+uue//lTA/CV1V/3/M837aKvvv6653++UQvmauuve/7nTwfAV1N/3fM/fzr24Cuuv+75nz8FFnxl9dc9//MOr/8/glixwRuUfM4AAAAASUVORK5CYII='; } - getSearchTexture() { + /** + * Returns the search texture as a Base64 string.. + * + * @private + * @return {String} The search texture. + */ + _getSearchTexture() { return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEIAAAAhCAAAAABIXyLAAAAAOElEQVRIx2NgGAWjYBSMglEwEICREYRgFBZBqDCSLA2MGPUIVQETE9iNUAqLR5gIeoQKRgwXjwAAGn4AtaFeYLEAAAAASUVORK5CYII='; diff --git a/examples/jsm/tsl/display/SSAAPassNode.js b/examples/jsm/tsl/display/SSAAPassNode.js index 39fc16868f3d91..477bb97dfec751 100644 --- a/examples/jsm/tsl/display/SSAAPassNode.js +++ b/examples/jsm/tsl/display/SSAAPassNode.js @@ -1,20 +1,23 @@ import { AdditiveBlending, Color, Vector2, PostProcessingUtils, PassNode, QuadMesh, NodeMaterial } from 'three/webgpu'; import { nodeObject, uniform, mrt, texture, getTextureIndex } from 'three/tsl'; +/** @module SSAAPassNode **/ + const _size = /*@__PURE__*/ new Vector2(); let _rendererState; /** -* -* Supersample Anti-Aliasing Render Pass -* -* This manual approach to SSAA re-renders the scene ones for each sample with camera jitter and accumulates the results. -* -* References: https://en.wikipedia.org/wiki/Supersampling -* -*/ - + * A special render pass node that renders the scene with SSAA (Supersampling Anti-Aliasing). + * This manual SSAA approach re-renders the scene ones for each sample with camera jitter and accumulates the results. + * + * This node produces a high-quality anti-aliased output but is also extremely expensive because of + * its brute-force approach of re-rendering the entire scene multiple times. + * + * References: https://en.wikipedia.org/wiki/Supersampling + * + * @augments PassNode + */ class SSAAPassNode extends PassNode { static get type() { @@ -23,25 +26,89 @@ class SSAAPassNode extends PassNode { } + /** + * Constructs a new SSAA pass node. + * + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + */ constructor( scene, camera ) { super( PassNode.COLOR, scene, camera ); + /** + * This flag can be used for type testing. + * + * @type {Boolean} + * @readonly + * @default true + */ this.isSSAAPassNode = true; - this.sampleLevel = 4; // specified as n, where the number of samples is 2^n, so sampleLevel = 4, is 2^4 samples, 16. + /** + * The sample level specified as n, where the number of samples is 2^n, + * so sampleLevel = 4, is 2^4 samples, 16. + * + * @type {Number} + * @default 4 + */ + this.sampleLevel = 4; + + /** + * Whether rounding erros should be mitigated or not. + * + * @type {Boolean} + * @default true + */ this.unbiased = true; + + /** + * The clear color of the pass. + * + * @type {Color} + * @default 0x000000 + */ this.clearColor = new Color( 0x000000 ); + + /** + * The clear alpha of the pass. + * + * @type {Number} + * @default 0 + */ this.clearAlpha = 0; + /** + * A uniform node representing the sample weight. + * + * @type {UnifornNode} + * @default 1 + */ this.sampleWeight = uniform( 1 ); - this.sampleRenderTarget = null; - + /** + * Reference to the internal render target that holds the current sample. + * + * @private + * @type {RenderTarget?} + */ + this._sampleRenderTarget = null; + + /** + * Reference to the internal quad mesh. + * + * @private + * @type {QuadMesh} + */ this._quadMesh = new QuadMesh(); } + /** + * This method is used to render the SSAA effect once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ updateBefore( frame ) { const { renderer } = frame; @@ -56,7 +123,7 @@ class SSAAPassNode extends PassNode { const size = renderer.getSize( _size ); this.setSize( size.width, size.height ); - this.sampleRenderTarget.setSize( this.renderTarget.width, this.renderTarget.height ); + this._sampleRenderTarget.setSize( this.renderTarget.width, this.renderTarget.height ); // @@ -120,7 +187,7 @@ class SSAAPassNode extends PassNode { } renderer.setClearColor( this.clearColor, this.clearAlpha ); - renderer.setRenderTarget( this.sampleRenderTarget ); + renderer.setRenderTarget( this._sampleRenderTarget ); renderer.clear(); renderer.render( scene, camera ); @@ -139,7 +206,7 @@ class SSAAPassNode extends PassNode { } - renderer.copyTextureToTexture( this.sampleRenderTarget.depthTexture, this.renderTarget.depthTexture ); + renderer.copyTextureToTexture( this._sampleRenderTarget.depthTexture, this.renderTarget.depthTexture ); // restore @@ -167,11 +234,17 @@ class SSAAPassNode extends PassNode { } + /** + * This method is used to setup the effect's MRT configuration and quad mesh. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {PassTextureNode} + */ setup( builder ) { - if ( this.sampleRenderTarget === null ) { + if ( this._sampleRenderTarget === null ) { - this.sampleRenderTarget = this.renderTarget.clone(); + this._sampleRenderTarget = this.renderTarget.clone(); } @@ -185,11 +258,11 @@ class SSAAPassNode extends PassNode { for ( const name in passMRT.outputNodes ) { - const index = getTextureIndex( this.sampleRenderTarget.textures, name ); + const index = getTextureIndex( this._sampleRenderTarget.textures, name ); if ( index >= 0 ) { - outputs[ name ] = texture( this.sampleRenderTarget.textures[ index ] ).mul( this.sampleWeight ); + outputs[ name ] = texture( this._sampleRenderTarget.textures[ index ] ).mul( this.sampleWeight ); } @@ -199,7 +272,7 @@ class SSAAPassNode extends PassNode { } else { - sampleTexture = texture( this.sampleRenderTarget.texture ).mul( this.sampleWeight ); + sampleTexture = texture( this._sampleRenderTarget.texture ).mul( this.sampleWeight ); } @@ -216,13 +289,17 @@ class SSAAPassNode extends PassNode { } + /** + * Frees internal resources. This method should be called + * when the pass is no longer required. + */ dispose() { super.dispose(); - if ( this.sampleRenderTarget !== null ) { + if ( this._sampleRenderTarget !== null ) { - this.sampleRenderTarget.dispose(); + this._sampleRenderTarget.dispose(); } @@ -269,4 +346,12 @@ const _JitterVectors = [ ] ]; +/** + * TSL function for creating a SSAA pass node for Supersampling Anti-Aliasing. + * + * @function + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + * @returns {SSAAPassNode} + */ export const ssaaPass = ( scene, camera ) => nodeObject( new SSAAPassNode( scene, camera ) ); diff --git a/examples/jsm/tsl/display/Sepia.js b/examples/jsm/tsl/display/Sepia.js index cae66b62224de3..939de4fd2d474e 100644 --- a/examples/jsm/tsl/display/Sepia.js +++ b/examples/jsm/tsl/display/Sepia.js @@ -1,5 +1,14 @@ import { dot, Fn, vec3, vec4 } from 'three/tsl'; +/** @module Sepia **/ + +/** + * Applies a sepia effect to the given color node. + * + * @function + * @param {Node} color - The color node to apply the sepia for. + * @return {Node} The updated color node. + */ export const sepia = /*@__PURE__*/ Fn( ( [ color ] ) => { const c = vec3( color ); diff --git a/examples/jsm/tsl/display/SobelOperatorNode.js b/examples/jsm/tsl/display/SobelOperatorNode.js index eb40fd4a27cc80..a065d2822e9c6b 100644 --- a/examples/jsm/tsl/display/SobelOperatorNode.js +++ b/examples/jsm/tsl/display/SobelOperatorNode.js @@ -1,6 +1,15 @@ import { Vector2, TempNode, NodeUpdateType } from 'three/webgpu'; import { nodeObject, Fn, uv, uniform, convertToTexture, vec2, vec3, vec4, mat3, luminance, add } from 'three/tsl'; +/** @module SobelOperatorNode **/ + +/** + * Post processing node for detecting edges with a sobel filter. + * A sobel filter should be applied after tone mapping and output color + * space conversion. + * + * @augments TempNode + */ class SobelOperatorNode extends TempNode { static get type() { @@ -9,19 +18,47 @@ class SobelOperatorNode extends TempNode { } + /** + * Constructs a new sobel operator node. + * + * @param {TextureNode} textureNode - The texture node that represents the input of the effect. + */ constructor( textureNode ) { super( 'vec4' ); + /** + * The texture node that represents the input of the effect. + * + * @type {TextureNode} + */ this.textureNode = textureNode; + /** + * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node updates + * its internal uniforms once per frame in `updateBefore()`. + * + * @type {String} + * @default 'frame' + */ this.updateBeforeType = NodeUpdateType.FRAME; + /** + * A uniform node holding the inverse resolution value. + * + * @private + * @type {UniformNode} + */ this._invSize = uniform( new Vector2() ); } - updateBefore() { + /** + * This method is used to update the effect's uniforms once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ + updateBefore( /* frame */ ) { const map = this.textureNode.value; @@ -29,7 +66,13 @@ class SobelOperatorNode extends TempNode { } - setup() { + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {ShaderCallNodeInternal} + */ + setup( /* builder */ ) { const { textureNode } = this; @@ -115,4 +158,11 @@ class SobelOperatorNode extends TempNode { export default SobelOperatorNode; +/** + * TSL function for creating a sobel operator node which performs edge detection with a sobel filter. + * + * @function + * @param {Node} node - The node that represents the input of the effect. + * @returns {SobelOperatorNode} + */ export const sobel = ( node ) => nodeObject( new SobelOperatorNode( convertToTexture( node ) ) ); diff --git a/examples/jsm/tsl/display/StereoCompositePassNode.js b/examples/jsm/tsl/display/StereoCompositePassNode.js index 56c793c7c376bd..329649a87ac09c 100644 --- a/examples/jsm/tsl/display/StereoCompositePassNode.js +++ b/examples/jsm/tsl/display/StereoCompositePassNode.js @@ -6,6 +6,16 @@ const _quadMesh = /*@__PURE__*/ new QuadMesh(); let _rendererState; +/** + * A special (abstract) render pass node that renders the scene + * as a stereoscopic image. Unlike {@link StereoPassNode}, this + * node composits the image for the left and right eye + * into a single one. That is required for effects like + * anaglyph or parallax barrier. + * + * @abstract + * @augments PassNode + */ class StereoCompositePassNode extends PassNode { static get type() { @@ -14,25 +24,76 @@ class StereoCompositePassNode extends PassNode { } + /** + * Constructs a new stereo composite pass node. + * + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + */ constructor( scene, camera ) { super( PassNode.COLOR, scene, camera ); + /** + * This flag can be used for type testing. + * + * @type {Boolean} + * @readonly + * @default true + */ this.isStereoCompositePassNode = true; + /** + * The internal stereo camera that is used to render the scene. + * + * @type {StereoCamera} + */ this.stereo = new StereoCamera(); const _params = { minFilter: LinearFilter, magFilter: NearestFilter, type: HalfFloatType }; + /** + * The render target for rendering the left eye's view. + * + * @type {RenderTarget} + */ this._renderTargetL = new RenderTarget( 1, 1, _params ); + + /** + * The render target for rendering the right eye's view. + * + * @type {RenderTarget} + */ this._renderTargetR = new RenderTarget( 1, 1, _params ); + /** + * A texture node representing the left's eye view. + * + * @type {TextureNode} + */ this._mapLeft = texture( this._renderTargetL.texture ); + + /** + * A texture node representing the right's eye view. + * + * @type {TextureNode} + */ this._mapRight = texture( this._renderTargetR.texture ); + /** + * The node material that implements the composite. All + * derived effect passes must provide an instance for rendering. + * + * @type {NodeMaterial} + */ this._material = null; } + /** + * Updates the internal stereo camera. + * + * @param {Number} coordinateSystem - The current coordinate system. + */ updateStereoCamera( coordinateSystem ) { this.stereo.cameraL.coordinateSystem = coordinateSystem; @@ -41,6 +102,12 @@ class StereoCompositePassNode extends PassNode { } + /** + * Sets the size of the pass. + * + * @param {Number} width - The width of the pass. + * @param {Number} height - The height of the pass. + */ setSize( width, height ) { super.setSize( width, height ); @@ -50,6 +117,11 @@ class StereoCompositePassNode extends PassNode { } + /** + * This method is used to render the effect once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ updateBefore( frame ) { const { renderer } = frame; @@ -88,6 +160,10 @@ class StereoCompositePassNode extends PassNode { } + /** + * Frees internal resources. This method should be called + * when the pass is no longer required. + */ dispose() { super.dispose(); diff --git a/examples/jsm/tsl/display/StereoPassNode.js b/examples/jsm/tsl/display/StereoPassNode.js index 912d1d40e3b7dd..eb486e3bcffa5c 100644 --- a/examples/jsm/tsl/display/StereoPassNode.js +++ b/examples/jsm/tsl/display/StereoPassNode.js @@ -1,10 +1,17 @@ import { StereoCamera, Vector2, PassNode, PostProcessingUtils } from 'three/webgpu'; import { nodeObject } from 'three/tsl'; +/** @module StereoPassNode **/ + const _size = /*@__PURE__*/ new Vector2(); let _rendererState; +/** + * A special render pass node that renders the scene as a stereoscopic image. + * + * @augments PassNode + */ class StereoPassNode extends PassNode { static get type() { @@ -13,17 +20,40 @@ class StereoPassNode extends PassNode { } + /** + * Constructs a new stereo pass node. + * + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + */ constructor( scene, camera ) { super( PassNode.COLOR, scene, camera ); + /** + * This flag can be used for type testing. + * + * @type {Boolean} + * @readonly + * @default true + */ this.isStereoPassNode = true; + /** + * The internal stereo camera that is used to render the scene. + * + * @type {StereoCamera} + */ this.stereo = new StereoCamera(); this.stereo.aspect = 0.5; } + /** + * This method is used to render the stereo effect once per frame. + * + * @param {NodeFrame} frame - The current node frame. + */ updateBefore( frame ) { const { renderer } = frame; @@ -79,4 +109,12 @@ class StereoPassNode extends PassNode { export default StereoPassNode; +/** + * TSL function for creating a stereo pass node for stereoscopic rendering. + * + * @function + * @param {Scene} scene - The scene to render. + * @param {Camera} camera - The camera to render the scene with. + * @returns {StereoPassNode} + */ export const stereoPass = ( scene, camera ) => nodeObject( new StereoPassNode( scene, camera ) ); diff --git a/examples/jsm/tsl/display/TransitionNode.js b/examples/jsm/tsl/display/TransitionNode.js index 45b3a41c1bf2d8..004bf43f733281 100644 --- a/examples/jsm/tsl/display/TransitionNode.js +++ b/examples/jsm/tsl/display/TransitionNode.js @@ -1,6 +1,13 @@ import { TempNode } from 'three/webgpu'; import { nodeObject, Fn, float, uv, convertToTexture, vec4, If, int, clamp, sub, mix } from 'three/tsl'; +/** @module TransitionNode **/ + +/** + * Post processing node for creating a transition effect between scenes. + * + * @augments TempNode + */ class TransitionNode extends TempNode { static get type() { @@ -9,24 +16,70 @@ class TransitionNode extends TempNode { } + /** + * Constructs a new transition node. + * + * @param {TextureNode} textureNodeA - A texture node that represents the beauty pass of the first scene. + * @param {TextureNode} textureNodeB - A texture node that represents the beauty pass of the second scene. + * @param {TextureNode} mixTextureNode - A texture node that defines how the transition effect should look like. + * @param {Node} mixRatioNode - The interpolation factor that controls the mix. + * @param {Node} thresholdNode - Can be used to tweak the linear interpolation. + * @param {Node} useTextureNode - Whether `mixTextureNode` should influence the transition or not. + */ constructor( textureNodeA, textureNodeB, mixTextureNode, mixRatioNode, thresholdNode, useTextureNode ) { super( 'vec4' ); - // Input textures - + /** + * A texture node that represents the beauty pass of the first scene. + * + * @type {TextureNode} + */ this.textureNodeA = textureNodeA; + + /** + * A texture node that represents the beauty pass of the second scene. + * + * @type {TextureNode} + */ this.textureNodeB = textureNodeB; - this.mixTextureNode = mixTextureNode; - // Uniforms + /** + * A texture that defines how the transition effect should look like. + * + * @type {TextureNode} + */ + this.mixTextureNode = mixTextureNode; + /** + * The interpolation factor that controls the mix. + * + * @type {Node} + */ this.mixRatioNode = mixRatioNode; + + /** + * Can be used to tweak the linear interpolation. + * + * @type {Node} + */ this.thresholdNode = thresholdNode; + + /** + * Whether `mixTextureNode` should influence the transition or not. + * + * @type {Node} + */ this.useTextureNode = useTextureNode; } + /** + * This method is used to setup the effect's TSL code. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {ShaderCallNodeInternal} + */ setup() { const { textureNodeA, textureNodeB, mixTextureNode, mixRatioNode, thresholdNode, useTextureNode } = this; @@ -73,4 +126,16 @@ class TransitionNode extends TempNode { export default TransitionNode; -export const transition = ( nodeA, nodeB, mixTexture, mixRatio = 0.0, threshold = 0.1, useTexture = 0 ) => nodeObject( new TransitionNode( convertToTexture( nodeA ), convertToTexture( nodeB ), convertToTexture( mixTexture ), nodeObject( mixRatio ), nodeObject( threshold ), nodeObject( useTexture ) ) ); +/** + * TSL function for creating a transition node for post processing. + * + * @function + * @param {Node} nodeA - A texture node that represents the beauty pass of the first scene. + * @param {Node} nodeB - A texture node that represents the beauty pass of the second scene. + * @param {Node} mixTextureNode - A texture that defines how the transition effect should look like. + * @param {Node | Number} mixRatio - The interpolation factor that controls the mix. + * @param {Node | Number} threshold - Can be used to tweak the linear interpolation. + * @param {Node | Number} useTexture - Whether `mixTextureNode` should influence the transition or not. + * @returns {TransitionNode} + */ +export const transition = ( nodeA, nodeB, mixTextureNode, mixRatio, threshold, useTexture ) => nodeObject( new TransitionNode( convertToTexture( nodeA ), convertToTexture( nodeB ), convertToTexture( mixTextureNode ), nodeObject( mixRatio ), nodeObject( threshold ), nodeObject( useTexture ) ) ); diff --git a/examples/jsm/tsl/display/hashBlur.js b/examples/jsm/tsl/display/hashBlur.js index 6a9eaea079686d..eb4426c482a3cd 100644 --- a/examples/jsm/tsl/display/hashBlur.js +++ b/examples/jsm/tsl/display/hashBlur.js @@ -1,7 +1,18 @@ import { float, Fn, vec2, uv, sin, rand, degrees, cos, Loop, vec4 } from 'three/tsl'; -// https://www.shadertoy.com/view/4lXXWn - +/** @module HashBlur **/ + +/** + * Applies a hash blur effect to the given texture node. + * + * Reference: {@link https://www.shadertoy.com/view/4lXXWn}. + * + * @function + * @param {Node} textureNode - The texture node that should be blurred. + * @param {Node} [bluramount=float(0.1)] - This node determines the amount of blur. + * @param {Node} [repeats=float(45)] - This node determines the quality of the blur. A higher value produces a less grainy result but is also more expensive. + * @return {Node} The blurred texture node. + */ export const hashBlur = /*#__PURE__*/ Fn( ( [ textureNode, bluramount = float( 0.1 ), repeats = float( 45 ) ] ) => { const draw = ( uv ) => textureNode.sample( uv ); diff --git a/examples/screenshots/webgpu_postprocessing.jpg b/examples/screenshots/webgpu_postprocessing.jpg index 33d1d0850c7172..305c5bb4bd366d 100644 Binary files a/examples/screenshots/webgpu_postprocessing.jpg and b/examples/screenshots/webgpu_postprocessing.jpg differ