{"componentChunkName":"component---node-modules-narative-gatsby-theme-novela-src-templates-author-template-tsx","path":"/authors/rick/page/4","result":{"pageContext":{"author":{"authorsPage":true,"bio":"I am a creative frontend developer, I specialise in React, GLSL, postprocessing and R3F. I love to experiment with code and deal with complex topics.\n","id":"a2f54938-ce4a-58e3-a14f-42c21df2482a","name":"Rick","featured":true,"social":[{"url":"https://twitter.com/TheFrontDev"},{"url":"https://github.com/Richard-Thompson"}],"slug":"/authors/rick","avatar":{"small":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAaABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAIBAwQF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABRlsKyA51mANxhD//xAAcEAACAgIDAAAAAAAAAAAAAAABAgMSAAQREyD/2gAIAQEAAQUCCLCqVkRYwROt5aDCak7DFn2Dx3+P/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAHBAAAgMAAwEAAAAAAAAAAAAAAAECESEQIDEy/9oACAEBAAY/AnuH1fEU/KIuMUtQ0WyoYedP/8QAGxABAAIDAQEAAAAAAAAAAAAAAQARITFBUSD/2gAIAQEAAT8hv6qbzGnBGvZesblRLKajqgyHnSZETPIHoo4SmR6uJW1r7fx//9oADAMBAAIAAwAAABCQyAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/EB//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/EB//xAAeEAEAAgICAwEAAAAAAAAAAAABABEhMUFhIFGRsf/aAAgBAQABPxDOQFt9a7gbWsLru1ycRUEAjJbBWGActkvYxwhbwlRCKSwoC5sGwsd/kpeSkMZPRL0ptbPzw//Z","aspectRatio":0.7647058823529411,"src":"/static/80e6d82107a1db055d4e32d8c709c04c/fa1ea/Rick.jpg","srcSet":"/static/80e6d82107a1db055d4e32d8c709c04c/afb2b/Rick.jpg 13w,\n/static/80e6d82107a1db055d4e32d8c709c04c/7c20e/Rick.jpg 25w,\n/static/80e6d82107a1db055d4e32d8c709c04c/fa1ea/Rick.jpg 50w,\n/static/80e6d82107a1db055d4e32d8c709c04c/03612/Rick.jpg 75w,\n/static/80e6d82107a1db055d4e32d8c709c04c/61cdf/Rick.jpg 100w,\n/static/80e6d82107a1db055d4e32d8c709c04c/0ff54/Rick.jpg 1200w","srcWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/e7b2c/Rick.webp","srcSetWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/58718/Rick.webp 13w,\n/static/80e6d82107a1db055d4e32d8c709c04c/74aad/Rick.webp 25w,\n/static/80e6d82107a1db055d4e32d8c709c04c/e7b2c/Rick.webp 50w,\n/static/80e6d82107a1db055d4e32d8c709c04c/ed320/Rick.webp 75w,\n/static/80e6d82107a1db055d4e32d8c709c04c/66016/Rick.webp 100w,\n/static/80e6d82107a1db055d4e32d8c709c04c/9000d/Rick.webp 1200w","sizes":"(max-width: 50px) 100vw, 50px"},"medium":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAaABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAIBAwQF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABRlsKyA51mANxhD//xAAcEAACAgIDAAAAAAAAAAAAAAABAgMSAAQREyD/2gAIAQEAAQUCCLCqVkRYwROt5aDCak7DFn2Dx3+P/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAHBAAAgMAAwEAAAAAAAAAAAAAAAECESEQIDEy/9oACAEBAAY/AnuH1fEU/KIuMUtQ0WyoYedP/8QAGxABAAIDAQEAAAAAAAAAAAAAAQARITFBUSD/2gAIAQEAAT8hv6qbzGnBGvZesblRLKajqgyHnSZETPIHoo4SmR6uJW1r7fx//9oADAMBAAIAAwAAABCQyAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/EB//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/EB//xAAeEAEAAgICAwEAAAAAAAAAAAABABEhMUFhIFGRsf/aAAgBAQABPxDOQFt9a7gbWsLru1ycRUEAjJbBWGActkvYxwhbwlRCKSwoC5sGwsd/kpeSkMZPRL0ptbPzw//Z","aspectRatio":0.7575757575757576,"src":"/static/80e6d82107a1db055d4e32d8c709c04c/61cdf/Rick.jpg","srcSet":"/static/80e6d82107a1db055d4e32d8c709c04c/7c20e/Rick.jpg 25w,\n/static/80e6d82107a1db055d4e32d8c709c04c/fa1ea/Rick.jpg 50w,\n/static/80e6d82107a1db055d4e32d8c709c04c/61cdf/Rick.jpg 100w,\n/static/80e6d82107a1db055d4e32d8c709c04c/59538/Rick.jpg 150w,\n/static/80e6d82107a1db055d4e32d8c709c04c/fd013/Rick.jpg 200w,\n/static/80e6d82107a1db055d4e32d8c709c04c/0ff54/Rick.jpg 1200w","srcWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/66016/Rick.webp","srcSetWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/74aad/Rick.webp 25w,\n/static/80e6d82107a1db055d4e32d8c709c04c/e7b2c/Rick.webp 50w,\n/static/80e6d82107a1db055d4e32d8c709c04c/66016/Rick.webp 100w,\n/static/80e6d82107a1db055d4e32d8c709c04c/d9b14/Rick.webp 150w,\n/static/80e6d82107a1db055d4e32d8c709c04c/6b183/Rick.webp 200w,\n/static/80e6d82107a1db055d4e32d8c709c04c/9000d/Rick.webp 1200w","sizes":"(max-width: 100px) 100vw, 100px"},"large":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAbABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAECAwQF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABg1aVCDnTzVmwxB//xAAcEAACAgMBAQAAAAAAAAAAAAABAgADBBESEzH/2gAIAQEAAQUCCLSqFbEWsEXr3aUEJ5JyGLPkHXvKxsn7P//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8BH//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8BH//EABsQAAICAwEAAAAAAAAAAAAAAAABAiEQETEy/9oACAEBAAY/AndHreIp8IuKStDRtmoUcLz/AP/EABwQAQACAgMBAAAAAAAAAAAAAAEAESFBEFFhMf/aAAgBAQABPyG/qJvMalEGO5aivuyVEsi17FYkyHWyJlDOoGoo0Sks2uN21r3cGsTBHvH/2gAMAwEAAgADAAAAEOAFMP/EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8QH//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8QH//EAB4QAQACAwACAwAAAAAAAAAAAAEAESExQRBhUXGR/9oACAEBAAE/EOgIHnWvcZWNF17tdORkgCmxgpDIO2JkNMCFprBCQ1LCqA/mCCCO4l8SkMZPgh3MbW7+TJQfdQg2hB4//9k=","aspectRatio":0.7522935779816514,"src":"/static/80e6d82107a1db055d4e32d8c709c04c/ec46e/Rick.jpg","srcSet":"/static/80e6d82107a1db055d4e32d8c709c04c/a2637/Rick.jpg 82w,\n/static/80e6d82107a1db055d4e32d8c709c04c/15203/Rick.jpg 164w,\n/static/80e6d82107a1db055d4e32d8c709c04c/ec46e/Rick.jpg 328w,\n/static/80e6d82107a1db055d4e32d8c709c04c/b69a5/Rick.jpg 492w,\n/static/80e6d82107a1db055d4e32d8c709c04c/23a36/Rick.jpg 656w,\n/static/80e6d82107a1db055d4e32d8c709c04c/0ff54/Rick.jpg 1200w","srcWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/5a48e/Rick.webp","srcSetWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/2d087/Rick.webp 82w,\n/static/80e6d82107a1db055d4e32d8c709c04c/29d87/Rick.webp 164w,\n/static/80e6d82107a1db055d4e32d8c709c04c/5a48e/Rick.webp 328w,\n/static/80e6d82107a1db055d4e32d8c709c04c/42f2e/Rick.webp 492w,\n/static/80e6d82107a1db055d4e32d8c709c04c/dec03/Rick.webp 656w,\n/static/80e6d82107a1db055d4e32d8c709c04c/9000d/Rick.webp 1200w","sizes":"(max-width: 328px) 100vw, 328px"}}},"originalPath":"/authors/authors-rick","skip":6,"limit":6,"group":[{"id":"a8822938-01fb-5ad9-a09b-f143a6404734","slug":"/abstract-blob-shader-with-particles","secret":false,"title":"Abstract Blob Shader with Particles","author":"Rick","date":"August 8th, 2023","dateForSEO":"2023-08-08T00:00:00.000Z","timeToRead":4,"excerpt":"Thefrontdev's take on an abstract blob which mimics an explosive type of event in space. Written in GLSL and using noise/particles/points with smoke textures on them. All built with the Three.js builtin materials and using onBeforeCompile to get all the standard light calculations from the builtin material.","canonical_url":null,"subscription":true,"body":"var _excluded = [\"components\"];\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Abstract Blob Shader with Particles\",\n  \"author\": \"Rick\",\n  \"date\": \"2023-08-08T00:00:00.000Z\",\n  \"hero\": \"./images/ss-particles-explosion.png\",\n  \"excerpt\": \"Thefrontdev's take on an abstract blob which mimics an explosive type of event in space. Written in GLSL and using noise/particles/points with smoke textures on them. All built with the Three.js builtin materials and using onBeforeCompile to get all the standard light calculations from the builtin material.\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, _excluded);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"iframe\", {\n    src: \"https://codesandbox.io/embed/clever-microservice-2xjy3x?fontsize=14&hidenavigation=1&theme=dark&view=preview\",\n    style: {\n      \"width\": \"100%\",\n      \"height\": \"500px\",\n      \"border\": \"0\",\n      \"borderRadius\": \"4px\",\n      \"overflow\": \"hidden\",\n      \"maxWidth\": \"500px\"\n    },\n    title: \"clever-microservice-2xjy3x\",\n    allow: \"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\",\n    sandbox: \"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n  }), mdx(\"p\", null, \"There are two things going on in this codeSandbox: \"), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"Inner sphere blob which has noise applied to it \"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, \"A points mesh with the outer smokey particles \")), mdx(\"h2\", {\n    \"id\": \"inner-sphere-blob-which-has-noise-applied-to-it\"\n  }, \"Inner sphere blob which has noise applied to it\"), mdx(\"p\", null, \"So \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.clicktorelease.com/blog/vertex-displacement-noise-3d-webgl-glsl-three-js/\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"this\"), \" article gave me an idea to apply turbulence to the surface of a sphere and this is what gives it the super nova type look - that and noise + 2 mixed colors.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const SphereShell = () => {\\n  const matRef = useRef();\\n  const sphereShellRef = useRef();\\n\\n  const customShader = {\\n    uniforms: {\\n      time: {\\n        value: 0\\n      }\\n    }\\n  };\\n\\n  const OBC = useCallback(\\n    (shader) => {\\n      shader.uniforms = {\\n        ...shader.uniforms,\\n        ...customShader.uniforms\\n      };\\n\\n      shader.vertexShader = shader.vertexShader.replace(\\n        \\\"#include <clipping_planes_pars_vertex>\\\",\\n        /* glsl */ `\\n        \\n        #include <clipping_planes_pars_vertex>\\n        uniform float time;\\n\\n        varying vec2 vUv;\\n        varying vec3 vPosition;\\n        varying float vNoise; \\n\\n\\n        ${cnoise}\\n\\n        float turbulence( vec3 p ) {\\n\\n          float w = 100.0;\\n          float t = -.5;\\n\\n          for (float f = 1.0 ; f <= 10.0 ; f++ ){\\n            float power = pow( 2.0, f );\\n            t += abs( cnoise( vec4( power * p, 1.0 ) ) / power );\\n          }\\n\\n          return t;\\n\\n        }\\n\\n      \\n      `\\n      );\\n      shader.vertexShader = shader.vertexShader.replace(\\n        `#include <begin_vertex>`,\\n        `\\n        #include <begin_vertex>\\n\\n\\n        vUv = uv;\\n\\n        // get a turbulent 3d noise using the normal, normal to high freq\\n        float noise = 10.0 *  -.10 * turbulence( .5 * normal + time * 0.25);\\n        // get a 3d noise using the position, low frequency\\n        float b = .30 * cnoise( vec4(0.05 * position, 1.0) );\\n        // compose both noises\\n        float displacement = - 1. * noise + b;\\n      \\n        // move the position along the normal and transform it\\n        vec3 newPosition = position + normal * displacement;\\n        transformed = newPosition;\\n        vPosition = newPosition;\\n        vNoise = noise;\\n      `\\n      );\\n\\n      shader.fragmentShader = shader.fragmentShader.replace(\\n        \\\"#include <clipping_planes_pars_fragment>\\\",\\n        `\\n        #include <clipping_planes_pars_fragment>\\n\\n        uniform float time;\\n        varying float vNoise;\\n        varying vec3 vPosition;\\n\\n        ${cnoise}\\n      `\\n      );\\n      shader.fragmentShader = shader.fragmentShader.replace(\\n        `#include <alphamap_fragment>`,\\n        `\\n        #include <alphamap_fragment>\\n\\n        float scale = 10.0;\\n        float radius = mod(time * 4.0, 20.0);\\n\\n        float contrast = 20.2;\\n        vec3 blue = vec3(0.1,0.3,2.0) * vNoise * 0.1;\\n        vec3 lesserBlue = vec3(0.05, 0.15,1.0)* vNoise * 0.03;\\n\\n        float noise = vNoise + 0.3;\\n\\n        diffuseColor = vec4(\\n          mix(lesserBlue, blue , noise) * contrast + 0.2,\\n          mix(\\n           0.0,\\n            1.0, \\n            1.0 - (radius / 20.0)  \\n          )\\n        ) ;\\n      `\\n      );\\n\\n      matRef.current.userData.shader = shader;\\n    },\\n    [customShader.uniforms]\\n  );\\n\\n  useFrame(({ clock, gl }) => {\\n    if (matRef.current) {\\n      const shader = matRef.current.userData.shader;\\n\\n      if (shader) {\\n        shader.uniforms.time = {\\n          value: clock.elapsedTime\\n        };\\n      }\\n    }\\n\\n    if (sphereShellRef.current) {\\n      const scale = (clock.elapsedTime * 4.0) % 20.0;\\n      sphereShellRef.current.scale.set(scale, scale, scale);\\n    }\\n  });\\n\\n  return (\\n    <group>\\n      <ambientLight />\\n      <mesh ref={sphereShellRef}>\\n        <sphereGeometry args={[1.0, 100, 100]} />\\n        <meshStandardMaterial\\n          ref={matRef}\\n          onBeforeCompile={OBC}\\n          transparent\\n          side={THREE.DoubleSide}\\n        />\\n      </mesh>\\n    </group>\\n  );\\n};\\n\")), mdx(\"p\", null, \"This is an example of the official way to adapt builtin materials. Why is this a good way to do it?\"), mdx(\"p\", null, \"Well\\u2026 unless your a physicist or a computer scientist which can understand complicated equations and convert them into code (which is a hard and rare skill to have), then this is definitely the way to go.\"), mdx(\"p\", null, \"It injects code before the shader gets compiled and therefore allows you to inject snippets into positions of the built in shader files or chunks.\"), mdx(\"p\", null, \"The .replace builtin javascript string method, allows us to select a shader chunk name or piece of code we know is in the built in shader. Then replace it with itself and the new code.\"), mdx(\"p\", null, \"Unfortunately this does require some knowledge of what shader terms mean and quite a bit of playing around. For example instead of setting gl_FragColor it could be diffuseColor or transformed in the vertex shader instead of setting gl_Position.\"), mdx(\"p\", null, \"Because we don\\u2019t want to set the color or vertex position in the wrong place as this defeats the point of merging our code in seamlessly and taking advantage of all the light calculations. Its all very well creating a abstract shader but if you want nice colors and lighting you will need to implement it yourself or use this onBeforeCompile.\"), mdx(\"h2\", {\n    \"id\": \"update-uniforms-using-onbeforecompile\"\n  }, \"Update uniforms using onBeforeCompile\"), mdx(\"p\", null, \"This took me a while to get right!\"), mdx(\"p\", null, \"In the onBeforeCompile we use the materials userData object and we store a \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"reference\"), \" to the shader in the onBeforeCompile callback.\"), mdx(\"p\", null, \"We obtain the shader parameter from the first param of onBeforeCompile callback and then set it as a property on the userData object on the material. Important to note\\u2026 this now gives us a reference to the shader which we can now access the uniforms of the shader and update as you would in the normal way in the useFrame R3F hook.\"), mdx(\"p\", null, \"Sounds more complicated than it actually is and is one of the official ways to access and update uniforms using onBeforeCompile.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"// define the mesh with OBC\\n<mesh ref={sphereShellRef}>\\n    <sphereGeometry args={[1.0, 100, 100]} />\\n    <meshStandardMaterial\\n        ref={matRef}\\n        onBeforeCompile={OBC}\\n        transparent\\n        side={THREE.DoubleSide}\\n    />\\n</mesh>\\n\\n// set the shader object in OBC\\n// callback to material useData object\\n\\nconst OBC = useCallback(\\n    (shader) => {\\n        shader.uniforms = {\\n            ...shader.uniforms,\\n            ...customShader.uniforms\\n        };\\n\\n\\n        // shader modifications.....\\n\\n\\n        // Set the shader reference\\n        matRef.current.userData.shader = shader;\\n    },\\n    [customShader.uniforms]\\n  );\\n\")), mdx(\"h2\", {\n    \"id\": \"abstract-blob-vertex-shader\"\n  }, \"Abstract blob Vertex Shader\"), mdx(\"p\", null, \"This is where we apply turbulence and pass some varying\\u2019s to the fragment shader to create a nice colorful effect.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"shader.vertexShader = shader.vertexShader.replace(\\n    \\\"#include <clipping_planes_pars_vertex>\\\",\\n    /* glsl */ `\\n    \\n    #include <clipping_planes_pars_vertex>\\n    uniform float time;\\n\\n    varying vec2 vUv;\\n    varying vec3 vPosition;\\n    varying float vNoise; \\n\\n\\n    ${cnoise}\\n\\n    float turbulence( vec3 p ) {\\n\\n        float w = 100.0;\\n        float t = -.5;\\n\\n        for (float f = 1.0 ; f <= 10.0 ; f++ ){\\n        float power = pow( 2.0, f );\\n        t += abs( cnoise( vec4( power * p, 1.0 ) ) / power );\\n        }\\n\\n        return t;\\n\\n    }\\n\\n    \\n    `\\n);\\nshader.vertexShader = shader.vertexShader.replace(\\n    `#include <begin_vertex>`,\\n    `\\n    #include <begin_vertex>\\n\\n\\n    vUv = uv;\\n\\n    // get a turbulent 3d noise using the normal, normal to high freq\\n    float noise = 10.0 *  -.10 * turbulence( .5 * normal + time * 0.25);\\n    // get a 3d noise using the position, low frequency\\n    float b = .30 * cnoise( vec4(0.05 * position, 1.0) );\\n    // compose both noises\\n    float displacement = - 1. * noise + b;\\n    \\n    // move the position along the normal and transform it\\n    vec3 newPosition = position + normal * displacement;\\n    transformed = newPosition;\\n    vPosition = newPosition;\\n    vNoise = noise;\\n    `\\n);\\n\")), mdx(\"p\", null, \"So if we have a singular vertex or fragment shader, why do we have to do the replace twice?\"), mdx(\"p\", null, \"The first replace is for any functions, uniforms, varyings, defines or pragmas with glslify. And the second one is for any code going into the body of the main function in the shaders.\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"#include <clipping_planes_pars_vertex>\"), \" - is at the end of all the uniforms and is perfect place for putting all the top elements in.\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"#include <begin_vertex>\"), \" - is at a point in the main function where we can access the transformed vec3 which is before any matrix multiplications (as far as I know or can tell)\"), mdx(\"p\", null, \"So we are using the turbulence function and apply noise over time we want a quite large noise scale as we don\\u2019t want it too wavy like clouds. Then set this to the transformed vec3, which will get used in all the other shader chunks below it - \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"#include <begin_vertex>\"), \". \"), mdx(\"h2\", {\n    \"id\": \"abstract-blob-fragment-shader\"\n  }, \"Abstract blob Fragment Shader\"), mdx(\"p\", null, \"The fragment shader is where we add all the color and modify the opacity with noise.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"shader.fragmentShader = shader.fragmentShader.replace(\\n\\\"#include <clipping_planes_pars_fragment>\\\",\\n`\\n    #include <clipping_planes_pars_fragment>\\n\\n    uniform float time;\\n    varying float vNoise;\\n    varying vec3 vPosition;\\n\\n    ${cnoise}\\n`\\n);\\nshader.fragmentShader = shader.fragmentShader.replace(\\n`#include <alphamap_fragment>`,\\n`\\n    #include <alphamap_fragment>\\n\\n    float radius = mod(time * 4.0, 20.0);\\n\\n    float contrast = 20.2;\\n    vec3 blue = vec3(0.1,0.3,2.0) * vNoise * 0.1;\\n    vec3 lesserBlue = vec3(0.05, 0.15,1.0)* vNoise * 0.03;\\n\\n    float noise = vNoise + 0.3;\\n\\n    diffuseColor = vec4(\\n        mix(lesserBlue, blue , noise) * contrast + 0.2,\\n        mix(\\n        0.0,\\n        1.0, \\n        1.0 - (radius / 20.0)  \\n        )\\n    );\\n`\\n);\\n\")), mdx(\"p\", null, \"The top replace is as it was for the vertex shader, just the fragment shader with a different string to replace \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"#include <clipping_planes_pars_fragment>\"), \".\"), mdx(\"p\", null, \"The bottom replace is the interesting part!\"), mdx(\"p\", null, \"So we have an expanding noisy sphere by having an oscillating radius using the builtin \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"mod()\"), \" glsl math function, noting the 20.0 float.\"), mdx(\"p\", null, \"We want to increase the contrast i.e. make the bright parts brighter and the dark parts darker. Basically increase the difference of the spectrum of colors. \"), mdx(\"p\", null, \"Have a play and change it.\"), mdx(\"p\", null, \"We have two blue colors and use noise as a multiplier which gives us the nice set of colors to mix. Finally mixing these two colors with the builtin \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"mix()\"), \" glsl function with noise again - meaning lower values of noise will associate with one color and higher values of noise with the other color. \"), mdx(\"p\", null, \"In these circumstances noise is good.\"), mdx(\"p\", null, \"We increase the brightness of the mixed color  by adding a 0.2 float to it. This addition will make things lighter all over. Remember 1.0 is white and 0.0 is black.\"), mdx(\"p\", null, \"Below will make it so that the alpha channel gets smaller as you get further away from the origin.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-glsl\"\n  }, \"mix(\\n    0.0,\\n    1.0, \\n    1.0 - (radius / 20.0)  \\n)\\n\")), mdx(\"p\", null, \"The first param of \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"mix()\"), \" will be more dominent if the third param is closer to 0 i.e. the radius gets bigger ( noting this \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"mix()\"), \" function\\u2019s third param is in the range of 0-1). We divide the radius by 20.0, as above we mod\\u2019ed the time by 20.0. All this means the greater the radius the lower the alpha channel will be.\"), mdx(\"p\", null, \"And because we set the transparent prop on the material we can have transparent parts of the material. Important to also note that not setting this will make changing the alpha channel in the fragment shader useless.\"), mdx(\"h2\", {\n    \"id\": \"a-points-mesh-with-the-outer-smokey-particles\"\n  }, \"A points mesh with the outer smokey particles\"), mdx(\"p\", null, \"Im not going to spend a lot of time on this as I have covered it in another tutorial. \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.thefrontdev.co.uk/sublime-soft-particle-fog-effect\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"This\"), \" tutorial did soft particles - granted unlit particles like in this codesandbox, as a builtin points material doesn\\u2019t have light affecting it without calculating it manually (I read the light calculations are excluded as it has to do with normal calculations, or calculating normals for points rather than meshes is troublesome).\"), mdx(\"h2\", {\n    \"id\": \"particle-vertex-shader\"\n  }, \"Particle Vertex Shader\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"shader.vertexShader = shader.vertexShader.replace(\\n    \\\"#include <clipping_planes_pars_vertex>\\\",\\n    /* glsl */ `\\n    \\n    #include <clipping_planes_pars_vertex>\\n    uniform float time;\\n    uniform vec3 mousePos;\\n\\n    varying vec2 vUv;\\n    varying float noise;\\n    varying vec3 noisyPos;\\n    varying vec3 vNormal;\\n    varying float vDepth;\\n\\n\\n\\n    ${cnoise}\\n\\n    float turbulence( vec3 p ) {\\n\\n        float w = 100.0;\\n        float t = -.5;\\n\\n        for (float f = 1.0 ; f <= 10.0 ; f++ ){\\n        float power = pow( 2.0, f );\\n        t += abs( cnoise( vec4( power * p, 1.0 ) ) / power );\\n        }\\n\\n        return t;\\n\\n    }\\n\\n    float sdSphere( vec3 pos, float radius ) {\\n            return max(length(pos), 0.0) - radius;\\n    }\\n\\n    float quadraticInOut(float t) {\\n        float p = 2.0 * t * t;\\n        return t < 0.5 ? p : -p + (4.0 * t) - 1.0;\\n    }\\n\\n    `\\n);\\nshader.vertexShader = shader.vertexShader.replace(\\n    `#include <begin_vertex>`,\\n    /* glsl */ `\\n    #include <begin_vertex>\\n\\n    float uSpeed = 14.0;\\n    float uAmplitude = 9.1; \\n\\n    vUv = uv;\\n\\n    vec3 tempPos = position;\\n        \\n    float uCurlFreq = 40.010;\\n    vec3 sum = vec3(0.0,0.0,0.0);\\n    sum += cnoise(vec4(tempPos, 1.0) * uCurlFreq + time * 0.9);\\n    sum += cnoise(vec4(tempPos, 1.0) * uCurlFreq * 0.025) ;\\n    sum += cnoise(vec4(tempPos, 1.0) * uCurlFreq * 0.05) ;\\n    sum += cnoise(vec4(tempPos, 1.0) * uCurlFreq * .10) ;\\n    sum += cnoise(vec4(tempPos, 1.0) * uCurlFreq * .20) ;\\n    sum /=5.0;\\n    tempPos += sum;\\n\\n    transformed = tempPos;\\n\\n    // get a turbulent 3d noise using the normal, normal to high freq\\n    float noise = 10.0 *  -.10 * turbulence( .5 * normal );\\n    // get a 3d noise using the position, low frequency\\n    float b = .70 * cnoise( vec4(0.05 * position + vec3( 10.0 ) + time, 1.0) );\\n    // compose both noises\\n    float displacement = - 1. * noise + b;\\n    \\n    // move the position along the normal and transform it\\n    vec3 newPosition = position + normal * displacement * 10.0;\\n\\n    noisyPos = newPosition;\\n    vNormal = normal;\\n    vDepth = gl_Position.z / gl_Position.w;\\n\\n    `\\n);\\n\\nshader.vertexShader = shader.vertexShader.replace(\\n    `gl_PointSize = size;`,\\n    /* glsl */ `\\n    float repeatingRadius  = mod(time * 4.0, 20.0);\\n    // float eased = quadraticInOut(repeatingRadius / 20.0) * time * 3.0;\\n    float defaultPointSize = 0.01;\\n    float maxPointSize = 25.1;\\n    float minRange = 1.9;\\n    float outerRange = 2.2;\\n\\n    float sdf = sdSphere(noisyPos, repeatingRadius);\\n\\n    bool inside =  sdf < minRange && sdf > -minRange;\\n    bool outer = sdf < outerRange && sdf > minRange && sdf < -minRange && sdf > -outerRange;\\n\\n    if (inside) {\\n        gl_PointSize = mix(defaultPointSize, maxPointSize,   repeatingRadius / 20.0);\\n    } \\n\\n    else if (outer) {\\n        gl_PointSize = mix(maxPointSize, defaultPointSize, repeatingRadius / 20.0);\\n\\n    } else {\\n        gl_PointSize = defaultPointSize;\\n    }\\n    \\n\\n    \\n    `\\n);\\n\")), mdx(\"p\", null, \"Very quickly the top replace defines the properties like uniforms and functions etc, the middle replace adds the turbulence which we will utilize in the fragment shader and finally a third replace deals with an increasing gl_PointSize as the SDF checks if the points are in a certain distance from the crest of the wave. \"), mdx(\"h2\", {\n    \"id\": \"particle-fragment-shader\"\n  }, \"Particle Fragment Shader\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"shader.fragmentShader = shader.fragmentShader.replace(\\n    \\\"#include <clipping_planes_pars_fragment>\\\",\\n    /* glsl */ `\\n    #include <clipping_planes_pars_fragment>\\n\\n    uniform float time;\\n    uniform sampler2D smoke;\\n    varying vec3 noisyPos;\\n    varying vec3 vNormal;\\n    varying float vDepth;\\n\\n    float rand(vec2 co, float seed) {\\n        float a = 12.9898;\\n        float b = 78.233;\\n        float c = 43758.5453;\\n        float dt= dot(co.xy ,vec2(a,b));\\n        float sn= mod(dt, 3.14);\\n        return fract(sin(sn + seed) * c);\\n    }\\n\\n    vec2 rotateUV(vec2 uv, float rotation, vec2 mid) {\\n        return vec2(\\n        cos(rotation) * (uv.x - mid.x) + sin(rotation) * (uv.y - mid.y) + mid.x,\\n        cos(rotation) * (uv.y - mid.y) - sin(rotation) * (uv.x - mid.x) + mid.y\\n        );\\n    }\\n\\n    ${cnoise}\\n`\\n);\\nshader.fragmentShader = shader.fragmentShader.replace(\\n    `#include <premultiplied_alpha_fragment>`,\\n    /* glsl */ `\\n    #include <premultiplied_alpha_fragment>\\n\\n    float repeatingRadius  = mod(time * 4.0, 20.0);\\n\\n    // Easing produces 0-1 so have to scale back up\\n    // float eased = quadraticInOut(repeatingRadius / 20.0) * time * 2.0;\\n    vec3 colorInside = vec3(0.1,0.3,10.0) * 43.0 * cnoise(vec4(noisyPos, 1.0)) * (abs(length(normalize(noisyPos))));\\n    vec3 colorOutside = vec3(0.05, 0.15,0.2) * 0.15;\\n    float minRange = 1.9;\\n    float outerRange = 2.2;\\n\\n\\n    // Soft Particles\\n\\n    vec2 spriteSheetSize = vec2(1280.0, 768.0);   // In px\\n    vec2 spriteSize = vec2(256, 256.0);        // In px\\n    float index = 1.0;            // Sprite index in sprite sheet (0-...)\\n    float w = spriteSheetSize.x;\\n    float h = spriteSheetSize.y;\\n\\n    // Normalize sprite size (0.0-1.0)\\n    float dx = spriteSize.x / w;\\n    float dy = spriteSize.y / h;\\n\\n    // Figure out number of tile cols of sprite sheet\\n    float cols = w / spriteSize.x;\\n\\n    // From linear index to row/col pair\\n    float col = mod(index, cols);\\n    float row = floor(index / cols);\\n\\n    // Finally to UV texture coordinates\\n    vec2 uv = vec2(dx * gl_PointCoord.x + col * dx, 1.0 - dy - row * dy + dy * gl_PointCoord.y);\\n\\n    float alpha = texture2D(smoke, uv).a;\\n\\n    //If transparent, don't draw\\n    if (alpha < 0.01) discard;\\n\\n    vec3 outputColor = vec3(0.2,0.1,3.0);\\n    outputColor.b += 2.0;\\n\\n    gl_FragColor = vec4(outputColor, (1.0 - repeatingRadius / 20.0) * 0.0199 );\\n`\\n);\\n\\n\")), mdx(\"p\", null, \"Everything up to this code section is covered in the previous article.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-glsl\"\n  }, \"vec3 outputColor = vec3(0.2,0.1,3.0);\\noutputColor.b += 2.0;\\n\\ngl_FragColor = vec4(outputColor , (1.0 - repeatingRadius / 20.0) * 0.0199 );\\n\")), mdx(\"p\", null, \"We want the particles textures to have a blue tinge so we increase the blue channel compared to the other red/green channels and then manually increase its  blue channel again to emphasize its blue\\u2019ness. \"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-glsl\"\n  }, \"(1.0 - repeatingRadius / 20.0) * 0.0199\\n\")), mdx(\"p\", null, \"This code just means that as the radius (range 1.0-20.0) gets bigger the alpha gets smaller and more transparent, and we fine tune this with the multiplier \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"* 0.0199\"), \".\"), mdx(\"p\", null, \"Subtle is often better than bold floats or multipliers.\"), mdx(\"h2\", {\n    \"id\": \"particles-geometry-and-bufferattributes\"\n  }, \"Particles Geometry and BufferAttributes\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"useEffect(() => {\\n    const n = 40,\\n        n2 = n / 2; // particles spread in the cube\\n\\n    for (let i = 0; i < numPoints; i++) {\\n        // positions\\n\\n        const x = Math.random() * n - n2;\\n        const y = Math.random() * n - n2;\\n        const z = Math.random() * n - n2;\\n\\n        // positions.push(x, y, z);\\n\\n        positions[i * 3] = x;\\n        positions[i * 3 + 1] = y;\\n        positions[i * 3 + 2] = z;\\n    }\\n\\n    pointsRef.current.geometry.setAttribute(\\n        \\\"position\\\",\\n        new THREE.BufferAttribute(positions, 3)\\n    );\\n});\\n\")), mdx(\"p\", null, \"This small bit of code produces a range of points across a distance in 3D space, in this case 40 three.js units ( -20.0 - +20.0), into a Float32Array which is required for setting a buffer attribute. And we initialize the buffer attribute like so:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"<bufferGeometry args={[radius, 20, 20]}>\\n    <bufferAttribute\\n        attachObject={[\\\"attributes\\\", \\\"position\\\"]}\\n        array={new Float32Array(numPoints * 3)}\\n        itemSize={3}\\n        onUpdate={(self) => (self.needsUpdate = true)}\\n    />\\n</bufferGeometry>\\n\")), mdx(\"iframe\", {\n    src: \"https://codesandbox.io/embed/clever-microservice-2xjy3x?fontsize=14&hidenavigation=1&theme=dark&view=preview\",\n    style: {\n      \"width\": \"100%\",\n      \"height\": \"500px\",\n      \"border\": \"0\",\n      \"borderRadius\": \"4px\",\n      \"overflow\": \"hidden\",\n      \"maxWidth\": \"500px\"\n    },\n    title: \"clever-microservice-2xjy3x\",\n    allow: \"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\",\n    sandbox: \"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n  }), mdx(\"p\", null, \"Until next time!\"));\n}\n;\nMDXContent.isMDXComponent = true;","hero":{"full":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABzUlEQVQ4y62S0W/SUBTGW24pLdBtBaMLGnEDCoUySguFgYmJRo0vM2ZLFjUORjdAY/b/v32ecwtZNALG7OHmtvfc87vf+c5RFEXBA6/NQVVNIZXS5K4o6mr9B1BVhQQJkUE6nYWmGfSt01laxpNHxL8CVZnMEE1jmPkbyDD2ZSw5U3cDGabrllTGCZxoGjZy2cd4+uwVykfvCWpL+E6F7JWu52GaRQLmYFllNCrvMOxOEXWu4bdj1JwLAhYobkpL/qLy/odfZSDDDp+E6AXf8bK/xKk/QdC6hFP9CK9xjtrRWwnl0rcAk/LYMy657V4iChbo+zHG4QyjYALP/YxW4wKRP8OL5683+XgP1KgEvmQfVDAKF+j5c4T+EoPglqAxgahs7yvtMVruF6okv71kORoijf29MimK0WlP4TRnaLhX5OE3RN0Yg+4cw2COR0VXjk4yoxubIqRCIQx49U8YR3eoNydoeVRyeEOKbzAIf8KtfpB+892twLWPDLbyJYKekaKphI161Bxa494PFMgSbgg3bz2jG+cw6bQFoSUjUToM4By/QXgypQbdomjXkMnsraYhuxu4VslgTmC4St95UlywHTl7DGR17PmfTfkFKLxIhMwwz+oAAAAASUVORK5CYII=","aspectRatio":1.4131736526946108,"src":"/static/4a3bf79975b70155f342896e9858d68c/a1946/ss-particles-explosion.png","srcSet":"/static/4a3bf79975b70155f342896e9858d68c/5b37e/ss-particles-explosion.png 236w,\n/static/4a3bf79975b70155f342896e9858d68c/49058/ss-particles-explosion.png 472w,\n/static/4a3bf79975b70155f342896e9858d68c/a1946/ss-particles-explosion.png 944w,\n/static/4a3bf79975b70155f342896e9858d68c/030f1/ss-particles-explosion.png 1416w,\n/static/4a3bf79975b70155f342896e9858d68c/14ee0/ss-particles-explosion.png 1542w","srcWebp":"/static/4a3bf79975b70155f342896e9858d68c/99fbb/ss-particles-explosion.webp","srcSetWebp":"/static/4a3bf79975b70155f342896e9858d68c/77392/ss-particles-explosion.webp 236w,\n/static/4a3bf79975b70155f342896e9858d68c/1f177/ss-particles-explosion.webp 472w,\n/static/4a3bf79975b70155f342896e9858d68c/99fbb/ss-particles-explosion.webp 944w,\n/static/4a3bf79975b70155f342896e9858d68c/4a492/ss-particles-explosion.webp 1416w,\n/static/4a3bf79975b70155f342896e9858d68c/fbb14/ss-particles-explosion.webp 1542w","sizes":"(max-width: 944px) 100vw, 944px"},"regular":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABzUlEQVQ4y62S0W/SUBTGW24pLdBtBaMLGnEDCoUySguFgYmJRo0vM2ZLFjUORjdAY/b/v32ecwtZNALG7OHmtvfc87vf+c5RFEXBA6/NQVVNIZXS5K4o6mr9B1BVhQQJkUE6nYWmGfSt01laxpNHxL8CVZnMEE1jmPkbyDD2ZSw5U3cDGabrllTGCZxoGjZy2cd4+uwVykfvCWpL+E6F7JWu52GaRQLmYFllNCrvMOxOEXWu4bdj1JwLAhYobkpL/qLy/odfZSDDDp+E6AXf8bK/xKk/QdC6hFP9CK9xjtrRWwnl0rcAk/LYMy657V4iChbo+zHG4QyjYALP/YxW4wKRP8OL5683+XgP1KgEvmQfVDAKF+j5c4T+EoPglqAxgahs7yvtMVruF6okv71kORoijf29MimK0WlP4TRnaLhX5OE3RN0Yg+4cw2COR0VXjk4yoxubIqRCIQx49U8YR3eoNydoeVRyeEOKbzAIf8KtfpB+892twLWPDLbyJYKekaKphI161Bxa494PFMgSbgg3bz2jG+cw6bQFoSUjUToM4By/QXgypQbdomjXkMnsraYhuxu4VslgTmC4St95UlywHTl7DGR17PmfTfkFKLxIhMwwz+oAAAAASUVORK5CYII=","aspectRatio":1.4173913043478261,"src":"/static/4a3bf79975b70155f342896e9858d68c/3ddd4/ss-particles-explosion.png","srcSet":"/static/4a3bf79975b70155f342896e9858d68c/078a8/ss-particles-explosion.png 163w,\n/static/4a3bf79975b70155f342896e9858d68c/e56da/ss-particles-explosion.png 327w,\n/static/4a3bf79975b70155f342896e9858d68c/3ddd4/ss-particles-explosion.png 653w,\n/static/4a3bf79975b70155f342896e9858d68c/c5cc7/ss-particles-explosion.png 980w,\n/static/4a3bf79975b70155f342896e9858d68c/eebd2/ss-particles-explosion.png 1306w,\n/static/4a3bf79975b70155f342896e9858d68c/14ee0/ss-particles-explosion.png 1542w","srcWebp":"/static/4a3bf79975b70155f342896e9858d68c/0acdf/ss-particles-explosion.webp","srcSetWebp":"/static/4a3bf79975b70155f342896e9858d68c/ac59e/ss-particles-explosion.webp 163w,\n/static/4a3bf79975b70155f342896e9858d68c/7660b/ss-particles-explosion.webp 327w,\n/static/4a3bf79975b70155f342896e9858d68c/0acdf/ss-particles-explosion.webp 653w,\n/static/4a3bf79975b70155f342896e9858d68c/75470/ss-particles-explosion.webp 980w,\n/static/4a3bf79975b70155f342896e9858d68c/68d47/ss-particles-explosion.webp 1306w,\n/static/4a3bf79975b70155f342896e9858d68c/fbb14/ss-particles-explosion.webp 1542w","sizes":"(max-width: 653px) 100vw, 653px"},"narrow":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABzUlEQVQ4y62S0W/SUBTGW24pLdBtBaMLGnEDCoUySguFgYmJRo0vM2ZLFjUORjdAY/b/v32ecwtZNALG7OHmtvfc87vf+c5RFEXBA6/NQVVNIZXS5K4o6mr9B1BVhQQJkUE6nYWmGfSt01laxpNHxL8CVZnMEE1jmPkbyDD2ZSw5U3cDGabrllTGCZxoGjZy2cd4+uwVykfvCWpL+E6F7JWu52GaRQLmYFllNCrvMOxOEXWu4bdj1JwLAhYobkpL/qLy/odfZSDDDp+E6AXf8bK/xKk/QdC6hFP9CK9xjtrRWwnl0rcAk/LYMy657V4iChbo+zHG4QyjYALP/YxW4wKRP8OL5683+XgP1KgEvmQfVDAKF+j5c4T+EoPglqAxgahs7yvtMVruF6okv71kORoijf29MimK0WlP4TRnaLhX5OE3RN0Yg+4cw2COR0VXjk4yoxubIqRCIQx49U8YR3eoNydoeVRyeEOKbzAIf8KtfpB+892twLWPDLbyJYKekaKphI161Bxa494PFMgSbgg3bz2jG+cw6bQFoSUjUToM4By/QXgypQbdomjXkMnsraYhuxu4VslgTmC4St95UlywHTl7DGR17PmfTfkFKLxIhMwwz+oAAAAASUVORK5CYII=","aspectRatio":1.4074074074074074,"src":"/static/4a3bf79975b70155f342896e9858d68c/502b1/ss-particles-explosion.png","srcSet":"/static/4a3bf79975b70155f342896e9858d68c/f2e6d/ss-particles-explosion.png 114w,\n/static/4a3bf79975b70155f342896e9858d68c/4ddba/ss-particles-explosion.png 229w,\n/static/4a3bf79975b70155f342896e9858d68c/502b1/ss-particles-explosion.png 457w,\n/static/4a3bf79975b70155f342896e9858d68c/7ddc2/ss-particles-explosion.png 686w,\n/static/4a3bf79975b70155f342896e9858d68c/435bf/ss-particles-explosion.png 914w,\n/static/4a3bf79975b70155f342896e9858d68c/14ee0/ss-particles-explosion.png 1542w","srcWebp":"/static/4a3bf79975b70155f342896e9858d68c/15384/ss-particles-explosion.webp","srcSetWebp":"/static/4a3bf79975b70155f342896e9858d68c/31fce/ss-particles-explosion.webp 114w,\n/static/4a3bf79975b70155f342896e9858d68c/e3e25/ss-particles-explosion.webp 229w,\n/static/4a3bf79975b70155f342896e9858d68c/15384/ss-particles-explosion.webp 457w,\n/static/4a3bf79975b70155f342896e9858d68c/0258d/ss-particles-explosion.webp 686w,\n/static/4a3bf79975b70155f342896e9858d68c/64ea2/ss-particles-explosion.webp 914w,\n/static/4a3bf79975b70155f342896e9858d68c/fbb14/ss-particles-explosion.webp 1542w","sizes":"(max-width: 457px) 100vw, 457px"},"seo":{"src":"/static/4a3bf79975b70155f342896e9858d68c/6050d/ss-particles-explosion.png"}}},{"id":"838e76e9-f127-5195-9670-f8d99d7f2288","slug":"/realtime-displacement-maps-using-canvastextures-in-react-three-fiber","secret":false,"title":"Realtime Displacement Maps using CanvasTextures in React Three Fiber","author":"Rick","date":"August 8th, 2023","dateForSEO":"2023-08-08T00:00:00.000Z","timeToRead":2,"excerpt":"A very quick walkthrough for doing realtime displacement of a subdivided plane using displacement maps in Three.js. This has a wide number of applications, for example terrain tools much like in unity, real time growth patterns or some sort of animated displacement. Combined with emission and potential postprocessing you could create some pretty cool projects with this technique.","canonical_url":null,"subscription":true,"body":"var _excluded = [\"components\"];\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Realtime Displacement Maps using CanvasTextures in React Three Fiber\",\n  \"author\": \"Rick\",\n  \"date\": \"2023-08-08T00:00:00.000Z\",\n  \"hero\": \"./images/real-time-displacementMaps-hero.png\",\n  \"excerpt\": \"A very quick walkthrough for doing realtime displacement of a subdivided plane using displacement maps in Three.js. This has a wide number of applications, for example terrain tools much like in unity, real time growth patterns or some sort of animated displacement. Combined with emission and potential postprocessing you could create some pretty cool projects with this technique.\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, _excluded);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"What is a displacement map?\"), mdx(\"p\", null, \"This is a very valid question if you have never come across them before. It is a grey scale image, where white === higher and black === lowest. The material class in THREE has a displacementMap prop which we can use to apply the displacementMap (can be referred to a height map also - a little confusing). \"), mdx(\"p\", null, \"Usually you would use a powerful engine like blender or houdini to generate these grey scale images pre run time. But I wondered if you can update this performantly in some way in \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"real time\"), \" and you can! \"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"N.B.\"), \" The dashed line drawing on the canvas context is not mine im just using it to show an example of drawing a line / curve over time.\"), mdx(\"iframe\", {\n    src: \"https://codesandbox.io/embed/cool-saha-yd23h6?fontsize=14&hidenavigation=1&theme=dark&view=preview\",\n    style: {\n      \"width\": \"100%\",\n      \"height\": \"500px\",\n      \"border\": \"0\",\n      \"borderRadius\": \"4px\",\n      \"overflow\": \"hidden\",\n      \"maxWidth\": \"500px\"\n    },\n    title: \"cool-saha-yd23h6\",\n    allow: \"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\",\n    sandbox: \"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n  }), mdx(\"h2\", {\n    \"id\": \"workflow---canvas\"\n  }, \"Workflow - canvas\"), mdx(\"p\", null, \"So how is this possible without image editing software to create grey scale images on the fly?\"), mdx(\"p\", null, \"Drawing a line on a canvas context is pretty straight forward (with a bit of googling) and so is drawing this line over time. \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://jsfiddle.net/ud31ypsy/\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"This\"), \" jsfiddle is an example of this. Any you can modify the code to display on the map prop of the material to prove its working and showing.\"), mdx(\"p\", null, \"But how does this help? \"), mdx(\"p\", null, \"Well now we have an animation, all we have to do is somehow get a texture we can use in three/R3F everytime we increment the drawing in the context (using - JavaScripts setInterval). And in the color format expected - grey scale.\"), mdx(\"p\", null, \"We can set the colors in these lines:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const context = canvasHolder.getContext(\\\"2d\\\");\\nif (context) {\\n    context.rect(0, 0, canvasHolder.width, canvasHolder.height);\\n    context.fillStyle = \\\"black\\\";\\n    context.filter = \\\"blur(9px)\\\";\\n    context.fill();\\n}\\n\\n// And ....\\n\\ncontext.beginPath();\\ncontext.moveTo(100, 20);\\ncontext.lineTo(200, 160);\\ncontext.quadraticCurveTo(230, 200, 250, 120);\\ncontext.bezierCurveTo(290, -40, 300, 200, 400, 150);\\ncontext.lineTo(500, 90);\\ncontext.lineWidth = 5;\\ncontext.strokeStyle = \\\"white\\\";\\n\")), mdx(\"p\", null, \"The fillStyle, strokeStyle and blur control how our grey scale im age looks.\"), mdx(\"h2\", {\n    \"id\": \"canvas-textures-in-three\"\n  }, \"Canvas Textures in Three\"), mdx(\"p\", null, mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://threejs.org/docs/#api/en/textures/CanvasTexture\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"Here\"), \" is the page describing canvas textures in the three docs.\"), mdx(\"p\", null, \"It accepts a canvas as a parameter and gives us a texture just how we can use any other texture in the R3F components / uniforms. And guess what we have a canvas element in the DOM outside of the R3F canvas element..\"), mdx(\"p\", null, \"We can select this element and use this to draw to it (positioning this additional canvas offscreen, literally position fixed and right 100%).\"), mdx(\"p\", null, \"And its that simple!\"), mdx(\"p\", null, \"One small thing, I have to define the maps declaratively in the material jsx component like so:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"<meshStandardMaterial displacementScale={1.1}>\\n    <canvasTexture attach=\\\"map\\\" needsUpdate />\\n    <canvasTexture attach=\\\"displacementMap\\\" needsUpdate />\\n</meshStandardMaterial>\\n\")), mdx(\"p\", null, \"I think this is just like in the case where we setup buffer attributes for points bufferGeometry, we have to instantiate the textures. Not 100% sure on this, played around for a while to figure it out.\"), mdx(\"h2\", {\n    \"id\": \"update-the-maps-and-displacement-maps\"\n  }, \"Update the Maps and Displacement Maps\"), mdx(\"p\", null, \"So we essentially progress along a line and draw segments of it ; much like you animate a dashed svg line or used to (don\\u2019t know if theres another way of animating svgs these days). We do this over time using setInterval and clearing when we get to 100% progress\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const drawLine = () => {\\n    //this clears itself once the line is drawn\\n    lineInterval = setInterval(updateLine, 1);\\n};\\n\\nfunction updateLine() {\\n    //define the line\\n    defineLine();\\n\\n    if (progress < length) {\\n        progress += speed;\\n        moveDash(progress, dir);\\n\\n        if (canvas.current) {\\n            if (displacementMeshRef.current) {\\n                const texture = new THREE.CanvasTexture(canvas.current);\\n\\n                displacementMeshRef.current.material.map = texture;\\n                displacementMeshRef.current.material.displacementMap = texture;\\n            }\\n        }\\n    } else {\\n        clearInterval(lineInterval);\\n    }\\n}\\n\")), mdx(\"p\", null, \"Pretty simple setInterval setup and clearing the same interval.\"), mdx(\"p\", null, \"And we pass our canvas to canvasTexture every time the interval callback is called. Then use this to set the displacement and the map properties on the planes displacementMeshRef set below this.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const texture = new THREE.CanvasTexture(canvas.current);\\n\\ndisplacementMeshRef.current.material.map = texture;\\ndisplacementMeshRef.current.material.displacementMap = texture;\\n\")), mdx(\"p\", null, \"The scale is controlled by displacementScale prop on the material for the plane.\"), mdx(\"p\", null, \"And that pretty much it! \"), mdx(\"p\", null, \"This has some pretty powerful applications, like real time terrain tools and real time manipulation of vertices. How would you paint onto mesh and affect its colors or vertices?\"), mdx(\"p\", null, mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.thefrontdev.co.uk/dynamic-flow-map-painter-in-r3f-create-stunning-visualizations-with-real-time-interactivity\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"Heres\"), \" a former article which you might find useful.\"), mdx(\"p\", null, \"Until next time \\uD83D\\uDE42\"));\n}\n;\nMDXContent.isMDXComponent = true;","hero":{"full":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAIAAADUsmlHAAAACXBIWXMAAAsTAAALEwEAmpwYAAACGElEQVQ4y3WTvcuxcRTHL295HSj8CRKDkqJkMSCTwmgwIFbKYBGLlJeQvCSLlwyimORPIItBKcnCZLB5fZ5zdzy/67rd93OG07l+fM73nO/vuqg//4nH43G73Z7PJz5CsdlsEomE3+9/vV54SDEBOH3+C3yEvNvtcrmc2WwWCoUURUUiEexFw/Bwv99BjTDH47HdbttsNmRYLBaHw4Gi2+3Cr/DnNwwkAgBfr9flcmm328ViMTIAs9lshULB5/NFItF2u6WVETufz+Px2O12a7Vaj8djNBqdTqfFYlGr1cFgMBQKRaPRWCwWDofBCLLml3IqlZLL5agD2Wq1JpPJUqnU7/cLhQIwjUYD6lqtNp1OmR69YWAEAgGPx4NCp9PF4/FsNttqtQaDQT6fL5fLk8kknU73ej3cjob3+71MJiPKkGFg6Fiv18EeaNFsNovFIhyuViuy8BeMlc/nAwz9hACHwLNMJtPpdIbDIfASiUSlUqFB9D3jDLPZjKkMGbzV6/WBQKBarbpcLjgBt5gz026DvsFgQE3ksQUEaIIdUMC1kxumd8ZmYCxzcmwEgV3gttfrNXPhN4zih8NBKpWSsUlwuVzIDoeDue23dxv7wUv/IU72XywWH7I0jJPP53MmQxrBtX9Y9ctXBWEymYhtmDUazeVy+TnzNxhtrFQqKEgMH41GHyb/AmPj0+mkVCqJT16v9+eqJP4CQISNSra8s3wAAAAASUVORK5CYII=","aspectRatio":1.0976744186046512,"src":"/static/3f8844de3dc626f8421dc611aa79d99a/a1946/real-time-displacementMaps-hero.png","srcSet":"/static/3f8844de3dc626f8421dc611aa79d99a/5b37e/real-time-displacementMaps-hero.png 236w,\n/static/3f8844de3dc626f8421dc611aa79d99a/49058/real-time-displacementMaps-hero.png 472w,\n/static/3f8844de3dc626f8421dc611aa79d99a/a1946/real-time-displacementMaps-hero.png 944w,\n/static/3f8844de3dc626f8421dc611aa79d99a/acada/real-time-displacementMaps-hero.png 1376w","srcWebp":"/static/3f8844de3dc626f8421dc611aa79d99a/99fbb/real-time-displacementMaps-hero.webp","srcSetWebp":"/static/3f8844de3dc626f8421dc611aa79d99a/77392/real-time-displacementMaps-hero.webp 236w,\n/static/3f8844de3dc626f8421dc611aa79d99a/1f177/real-time-displacementMaps-hero.webp 472w,\n/static/3f8844de3dc626f8421dc611aa79d99a/99fbb/real-time-displacementMaps-hero.webp 944w,\n/static/3f8844de3dc626f8421dc611aa79d99a/53925/real-time-displacementMaps-hero.webp 1376w","sizes":"(max-width: 944px) 100vw, 944px"},"regular":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAIAAADUsmlHAAAACXBIWXMAAAsTAAALEwEAmpwYAAACGElEQVQ4y3WTvcuxcRTHL295HSj8CRKDkqJkMSCTwmgwIFbKYBGLlJeQvCSLlwyimORPIItBKcnCZLB5fZ5zdzy/67rd93OG07l+fM73nO/vuqg//4nH43G73Z7PJz5CsdlsEomE3+9/vV54SDEBOH3+C3yEvNvtcrmc2WwWCoUURUUiEexFw/Bwv99BjTDH47HdbttsNmRYLBaHw4Gi2+3Cr/DnNwwkAgBfr9flcmm328ViMTIAs9lshULB5/NFItF2u6WVETufz+Px2O12a7Vaj8djNBqdTqfFYlGr1cFgMBQKRaPRWCwWDofBCLLml3IqlZLL5agD2Wq1JpPJUqnU7/cLhQIwjUYD6lqtNp1OmR69YWAEAgGPx4NCp9PF4/FsNttqtQaDQT6fL5fLk8kknU73ej3cjob3+71MJiPKkGFg6Fiv18EeaNFsNovFIhyuViuy8BeMlc/nAwz9hACHwLNMJtPpdIbDIfASiUSlUqFB9D3jDLPZjKkMGbzV6/WBQKBarbpcLjgBt5gz026DvsFgQE3ksQUEaIIdUMC1kxumd8ZmYCxzcmwEgV3gttfrNXPhN4zih8NBKpWSsUlwuVzIDoeDue23dxv7wUv/IU72XywWH7I0jJPP53MmQxrBtX9Y9ctXBWEymYhtmDUazeVy+TnzNxhtrFQqKEgMH41GHyb/AmPj0+mkVCqJT16v9+eqJP4CQISNSra8s3wAAAAASUVORK5CYII=","aspectRatio":1.1013513513513513,"src":"/static/3f8844de3dc626f8421dc611aa79d99a/3ddd4/real-time-displacementMaps-hero.png","srcSet":"/static/3f8844de3dc626f8421dc611aa79d99a/078a8/real-time-displacementMaps-hero.png 163w,\n/static/3f8844de3dc626f8421dc611aa79d99a/e56da/real-time-displacementMaps-hero.png 327w,\n/static/3f8844de3dc626f8421dc611aa79d99a/3ddd4/real-time-displacementMaps-hero.png 653w,\n/static/3f8844de3dc626f8421dc611aa79d99a/c5cc7/real-time-displacementMaps-hero.png 980w,\n/static/3f8844de3dc626f8421dc611aa79d99a/eebd2/real-time-displacementMaps-hero.png 1306w,\n/static/3f8844de3dc626f8421dc611aa79d99a/acada/real-time-displacementMaps-hero.png 1376w","srcWebp":"/static/3f8844de3dc626f8421dc611aa79d99a/0acdf/real-time-displacementMaps-hero.webp","srcSetWebp":"/static/3f8844de3dc626f8421dc611aa79d99a/ac59e/real-time-displacementMaps-hero.webp 163w,\n/static/3f8844de3dc626f8421dc611aa79d99a/7660b/real-time-displacementMaps-hero.webp 327w,\n/static/3f8844de3dc626f8421dc611aa79d99a/0acdf/real-time-displacementMaps-hero.webp 653w,\n/static/3f8844de3dc626f8421dc611aa79d99a/75470/real-time-displacementMaps-hero.webp 980w,\n/static/3f8844de3dc626f8421dc611aa79d99a/68d47/real-time-displacementMaps-hero.webp 1306w,\n/static/3f8844de3dc626f8421dc611aa79d99a/53925/real-time-displacementMaps-hero.webp 1376w","sizes":"(max-width: 653px) 100vw, 653px"},"narrow":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAIAAADUsmlHAAAACXBIWXMAAAsTAAALEwEAmpwYAAACGElEQVQ4y3WTvcuxcRTHL295HSj8CRKDkqJkMSCTwmgwIFbKYBGLlJeQvCSLlwyimORPIItBKcnCZLB5fZ5zdzy/67rd93OG07l+fM73nO/vuqg//4nH43G73Z7PJz5CsdlsEomE3+9/vV54SDEBOH3+C3yEvNvtcrmc2WwWCoUURUUiEexFw/Bwv99BjTDH47HdbttsNmRYLBaHw4Gi2+3Cr/DnNwwkAgBfr9flcmm328ViMTIAs9lshULB5/NFItF2u6WVETufz+Px2O12a7Vaj8djNBqdTqfFYlGr1cFgMBQKRaPRWCwWDofBCLLml3IqlZLL5agD2Wq1JpPJUqnU7/cLhQIwjUYD6lqtNp1OmR69YWAEAgGPx4NCp9PF4/FsNttqtQaDQT6fL5fLk8kknU73ej3cjob3+71MJiPKkGFg6Fiv18EeaNFsNovFIhyuViuy8BeMlc/nAwz9hACHwLNMJtPpdIbDIfASiUSlUqFB9D3jDLPZjKkMGbzV6/WBQKBarbpcLjgBt5gz026DvsFgQE3ksQUEaIIdUMC1kxumd8ZmYCxzcmwEgV3gttfrNXPhN4zih8NBKpWSsUlwuVzIDoeDue23dxv7wUv/IU72XywWH7I0jJPP53MmQxrBtX9Y9ctXBWEymYhtmDUazeVy+TnzNxhtrFQqKEgMH41GHyb/AmPj0+mkVCqJT16v9+eqJP4CQISNSra8s3wAAAAASUVORK5CYII=","aspectRatio":1.0961538461538463,"src":"/static/3f8844de3dc626f8421dc611aa79d99a/502b1/real-time-displacementMaps-hero.png","srcSet":"/static/3f8844de3dc626f8421dc611aa79d99a/f2e6d/real-time-displacementMaps-hero.png 114w,\n/static/3f8844de3dc626f8421dc611aa79d99a/4ddba/real-time-displacementMaps-hero.png 229w,\n/static/3f8844de3dc626f8421dc611aa79d99a/502b1/real-time-displacementMaps-hero.png 457w,\n/static/3f8844de3dc626f8421dc611aa79d99a/7ddc2/real-time-displacementMaps-hero.png 686w,\n/static/3f8844de3dc626f8421dc611aa79d99a/435bf/real-time-displacementMaps-hero.png 914w,\n/static/3f8844de3dc626f8421dc611aa79d99a/acada/real-time-displacementMaps-hero.png 1376w","srcWebp":"/static/3f8844de3dc626f8421dc611aa79d99a/15384/real-time-displacementMaps-hero.webp","srcSetWebp":"/static/3f8844de3dc626f8421dc611aa79d99a/31fce/real-time-displacementMaps-hero.webp 114w,\n/static/3f8844de3dc626f8421dc611aa79d99a/e3e25/real-time-displacementMaps-hero.webp 229w,\n/static/3f8844de3dc626f8421dc611aa79d99a/15384/real-time-displacementMaps-hero.webp 457w,\n/static/3f8844de3dc626f8421dc611aa79d99a/0258d/real-time-displacementMaps-hero.webp 686w,\n/static/3f8844de3dc626f8421dc611aa79d99a/64ea2/real-time-displacementMaps-hero.webp 914w,\n/static/3f8844de3dc626f8421dc611aa79d99a/53925/real-time-displacementMaps-hero.webp 1376w","sizes":"(max-width: 457px) 100vw, 457px"},"seo":{"src":"/static/3f8844de3dc626f8421dc611aa79d99a/6050d/real-time-displacementMaps-hero.png"}}},{"id":"befeb601-efc3-5f85-92ca-6c77fa9c9180","slug":"/workflow-from-shadertoy-to-react-three-fiber-r3f","secret":false,"title":"Workflow from Shadertoy to React Three Fiber - R3F","author":"Rick","date":"August 7th, 2023","dateForSEO":"2023-08-07T00:00:00.000Z","timeToRead":5,"excerpt":"This is a workflow from shadertoy to r3f (@react-three/fiber). It involves ping pong textures and using the output texture as the input texture. This pattern is seen a lot in shadertoy as it uses textures and fragment shaders.","canonical_url":null,"subscription":true,"body":"var _excluded = [\"components\"];\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Workflow from Shadertoy to React Three Fiber - R3F\",\n  \"author\": \"Rick\",\n  \"date\": \"2023-08-07T00:00:00.000Z\",\n  \"hero\": \"./images/workflow-from-shadertoy-to-r3f.png\",\n  \"excerpt\": \"This is a workflow from shadertoy to r3f (@react-three/fiber). It involves ping pong textures and using the output texture as the input texture. This pattern is seen a lot in shadertoy as it uses textures and fragment shaders.\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, _excluded);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"I have tried to find a working version of this workflow for along time and is something I have tried multiple times but never really nailed it. \"), mdx(\"p\", null, \"I have a working version using \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.thefrontdev.co.uk/floodfill-growth-patterns-with-decals-and-dynamic-shaders\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"floodfill\"), \" techniques but it didn\\u2019t use purely R3F components. I finally have something which demonstrates texture feedback and written entirely in R3F without the native three.js style mixed in.\"), mdx(\"p\", null, \"A little side note is this idea came from a codepen \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://codepen.io/forerunrun/pen/poONERw\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"here\"), \". It was a fellow creative dev asking in the forums why the ping pong textures wasn\\u2019t working and someone kindly amended her code.\"), mdx(\"p\", null, \"This opens up a lot of doors for shadertoy shaders in R3F. \"), mdx(\"p\", null, \"The premise as stated involved using frame buffer objects which are just texture/images. Luckily @react-three/drei has a useFBO hook which quite neatly allows you to create frame buffer objects.\"), mdx(\"p\", null, \"I hear you saying what the hell is this?\"), mdx(\"p\", null, \"FBO or image or texture saves a frames pixels or data and can be passed around in a useFrame in such ways as to render the screen to it and then use in uniforms passing it to shaders.\"), mdx(\"p\", null, \"I found this workflow helped me to understand stateless highly parallel shaders and GPU architecture in a very practical way. \"), mdx(\"p\", null, \"In the same way you can access data about other pixels in the same frame using compute shaders (supported in upcoming webgpu, not WebGL2) - texture feedback allows you to save the frames data and pass it to the next frame.\"), mdx(\"p\", null, \"Theres always a catch though \\uD83D\\uDE00 \"), mdx(\"p\", null, \"The active FBO in a frame cannot be written too and read from in the same frame (this is, at least, my take on this with my limited understanding of the underlying mechanics of raw webgl).\"), mdx(\"p\", null, \"So what does this mean? Its a big no no to render/write to the buffer and then use this in a uniform and read/sample in a shader. It creates a bad feedback loop and it wont work. Or at least every time I tried this I got stuck at this stage (temporarily finding away to do it using flood fill a algorithm in an earlier article).\"), mdx(\"iframe\", {\n    src: \"https://codesandbox.io/embed/prod-tdd-vf4cy5?fontsize=14&hidenavigation=1&theme=dark&view=preview\",\n    style: {\n      \"width\": \"100%\",\n      \"height\": \"500px\",\n      \"border\": \"0\",\n      \"borderRadius\": \"4px\",\n      \"overflow\": \"hidden\",\n      \"maxWidth\": \"500px\"\n    },\n    title: \"prod-tdd-vf4cy5\",\n    allow: \"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\",\n    sandbox: \"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n  }), mdx(\"p\", null, \"As stated earlier the native threejs version of this is in \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://codepen.io/forerunrun/pen/poONERw\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"this\"), \" codepen. Im going to run through the basic ideas of the codesandbox above. \"), mdx(\"h2\", {\n    \"id\": \"ping-pong-textures---implementation\"\n  }, \"Ping Pong Textures - implementation\"), mdx(\"p\", null, \"Here is the offscreen scene component:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const OffScreenScene = forwardRef(function OffScreenScene(props, ref) {\\n  const { size } = useThree();\\n\\n  return (\\n    <group>\\n      <mesh ref={ref}>\\n        <planeGeometry args={[size.width, size.height]} />\\n        <offScreenMaterial\\n          bufferTexture={props.map}\\n          res={new THREE.Vector2(1024, 1024)}\\n          smokeSource={new THREE.Vector3(0, 0, 0)}\\n        />\\n      </mesh>\\n      <gridHelper />\\n    </group>\\n  );\\n});\\n\")), mdx(\"p\", null, \"and this is where it is used in the main scenes render method:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const [offScreenScene] = useState(() => new THREE.Scene());\\n\\n\\nrender (\\n    {createPortal(\\n        <>\\n            <OffScreenScene ref={offScreen} map={offScreenFBOTexture.texture} />\\n            <OrthographicCamera\\n                makeDefault\\n                position={[0, 0, 2]}\\n                args={[-1, 1, 1, -1, 1, 1000]}\\n                aspect={size.width / size.height}\\n                ref={cameraRef}\\n            />\\n        </>,\\n        offScreenScene\\n    )}\\n)\\n\")), mdx(\"p\", null, \"A lot to digest lets go through step by step.\"), mdx(\"p\", null, \"Offscreen rendering or render targets - this is a very common technique to do more advanced rendering or shaders. A potentially more well known demonstration of this workflow is GPU particles. \"), mdx(\"p\", null, \"Which will have a offscreen render target or simulationMaterial which uses the pixels in a FBO/texture/buffer to store positions or velocities/acceleration and then uses an orthographic camera faced directly down to render the offscreen scene . It is also a very very common practice in shadertoys shaders to have greater than 1 FBO/texture/buffer.\"), mdx(\"p\", null, \"And when you think about it, it is a quite in genius way to store some kind of state or data to be used in another shader or R3F material.\"), mdx(\"p\", null, \"So createPortal is a @react-three/drei helper function which will accept a component or jsx as the first parameter and a scene as the second parameter. The way to think of this is anything in this quote enclave (from the docs!) will not be rendered in the main scene, ideal for offscreen rendering.\"), mdx(\"p\", null, \"Why do we need to define this in the parent component or the onscreen component?\"), mdx(\"p\", null, \"Because we can pass refs/props down to the R3F components in the offscreen rendered scene. Very useful ey!\"), mdx(\"p\", null, \"Because we need to be able to access the material to set a map prop and setting the offscreen FBO.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"<mesh ref={ref}>\\n    <planeGeometry args={[size.width, size.height]} />\\n    <offScreenMaterial\\n        bufferTexture={props.map}\\n        res={new THREE.Vector2(1024, 1024)}\\n        smokeSource={new THREE.Vector3(0, 0, 0)}\\n    />\\n</mesh>\\n\")), mdx(\"p\", null, \"and then utilize this in the useFrame of the onscreen parent component like so:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"\\nconst offScreenFBOTexture = useFBO(1024, 1024, {\\n    minFilter: THREE.LinearFilter,\\n    magFilter: THREE.NearestFilter\\n});\\nconst onScreenFBOTexture = useFBO(1024, 1024, {\\n    minFilter: THREE.LinearFilter,\\n    magFilter: THREE.NearestFilter\\n});\\n\\nconst [offScreenScene] = useState(() => new THREE.Scene());\\nconst offScreenCameraRef = useRef(null);\\n\\nlet textureA = offScreenFBOTexture;\\nlet textureB = onScreenFBOTexture;\\n  \\nuseFrame(({ gl, camera }) => {\\n    gl.setRenderTarget(textureB);\\n    gl.render(offScreenScene, offScreenCameraRef.current);\\n\\n    //Swap textureA and B\\n    var t = textureA;\\n    textureA = textureB;\\n    textureB = t;\\n\\n    onScreen.current.material.map = textureB.texture;\\n    offScreen.current.material.uniforms.bufferTexture.value = textureA.texture;\\n\\n    gl.setRenderTarget(null);\\n    gl.render(scene, camera);\\n});\\n\")), mdx(\"p\", null, \"We have one offScreenFBOTexture and onScreenFBOTexture for the scenes.\"), mdx(\"p\", null, \"The offscreen scene is defined for the createPortal component:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const [offScreenScene] = useState(() => new THREE.Scene());\\n\")), mdx(\"p\", null, \"We also need to have access to the offscreen camera when we want to render using the renderer (gl) in R3F\\u2019s useFrame hook.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const offScreenCameraRef = useRef(null);\\n\\n// example \\nuseFrame(({ gl }) => {\\n    gl.render(scene, camera)\\n})\\n\")), mdx(\"p\", null, \"This gl doesn\\u2019t have limited use cases, this can be used for any three scene - offscreen or onscreen. \"), mdx(\"p\", null, \"Below shows how we render the offscreen scene into the offscreen render target and then swap or ping pong the textures. This avoids the known read/write limitation in current webgl2. Then the offscreen texture is set as the map prop for the onscreen material and we use the onscreen texture in the offscreen material/shader uniforms.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"//Swap textureA and B\\nvar t = textureA;\\ntextureA = textureB;\\ntextureB = t;\\n\\nonScreen.current.material.map = textureB.texture;\\noffScreen.current.material.uniforms.bufferTexture.value = textureA.texture;\\n\")), mdx(\"p\", null, \"Finally but by no means least, we have to set the render target back to null and render the scene again otherwise we would only be rendering the offscreen scene and not the onscreen scene.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"gl.setRenderTarget(null);\\ngl.render(scene, camera);\\n\")), mdx(\"h2\", {\n    \"id\": \"offscreen-orthographic-camera\"\n  }, \"Offscreen Orthographic Camera\"), mdx(\"p\", null, \"How do we know what camera to use for the offscreen render?\"), mdx(\"p\", null, \"This came of stack overflow and ill try and explain from what I know.\"), mdx(\"p\", null, \"A normal camera has perspective which can be difficult when wanting to deal with depths and trying to calculate this. This would not work 100% if the camera is facing the offscreen mesh at a 45 degree angle. \"), mdx(\"p\", null, \"It needs to be front on and without perspective! \"), mdx(\"p\", null, \"There comes into play the orthographic camera. It will face down naturally and we can control how much of the offscreen mesh/texture is rendered playing with the near/far and left/right/bottom/top arguments for the camera.\"), mdx(\"h2\", {\n    \"id\": \"interactivity\"\n  }, \"Interactivity?\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const onPointerMove = useCallback((e) => {\\n    const { uv } = e;\\n\\n    offScreen.current.material.uniforms.smokeSource.value.x = uv.x;\\n    offScreen.current.material.uniforms.smokeSource.value.y = uv.y;\\n}, []);\\n\\nconst onMouseUp = useCallback((event) => {\\n    offScreen.current.material.uniforms.smokeSource.value.z = 0.0;\\n}, []);\\nconst onMouseDown = useCallback((event) => {\\n    offScreen.current.material.uniforms.smokeSource.value.z = 0.1;\\n}, []);\\n\")), mdx(\"p\", null, \"And the onscreen mesh:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"<mesh\\n    ref={onScreen}\\n    onPointerMove={onPointerMove}\\n    onPointerDown={onMouseDown}\\n    onPointerUp={onMouseUp}\\n>\\n    <planeGeometry args={[20, 20]} />\\n    <meshBasicMaterial side={THREE.DoubleSide} map={onScreenFBOTexture} />\\n</mesh>\\n\")), mdx(\"p\", null, \"Thanks to the guys who maintain R3F/Three we have useful events for the jsx components which are powered with a raycaster under the hood fired from the mouse position into the scene.\"), mdx(\"p\", null, \"The event when a ray hits the mesh has loads of info so i encourage you to look around in the event object and play around with the data.\"), mdx(\"p\", null, \"I have used uvs as in these circumstances I find it easier to find the difference between uvs multiplied by the screen resolution, than try and get my head around coordinate systems which still somewhat confuse me if Im being honest.\"), mdx(\"p\", null, \"The onMouseUp and onMouseDown just determine when the mesh is clicked and stopped being clicked. Giving us the perfect switch between active effect and the non-active effect. \"), mdx(\"h2\", {\n    \"id\": \"shaders\"\n  }, \"Shaders\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const OffScreenMaterial = shaderMaterial(\\n  {\\n    bufferTexture: { value: null },\\n    res: { value: new THREE.Vector2(0, 0) },\\n    smokeSource: { value: new THREE.Vector3(0, 0, 0) }\\n  },\\n  // vertex shader\\n  /*glsl*/ `\\n    varying vec2 vUv;\\n    void main () {\\n        vUv =uv;\\n        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\\n    }\\n    `,\\n  // fragment shader\\n  /*glsl*/ `\\n    uniform vec2 res;//The width and height of our screen\\n    uniform sampler2D bufferTexture;//Our input texture\\n    uniform vec3 smokeSource;//The x,y are the posiiton. The z is the power/density\\n    varying vec2 vUv;\\n\\n    void main() {\\n      vec2 pixel = vUv;\\n      gl_FragColor = texture2D( bufferTexture, gl_FragCoord.xy / res.xy );\\n\\n      //Get the distance of the current pixel from the smoke source\\n      float dist = distance(pixel * res.xy, smokeSource.xy * res.xy);\\n\\n      gl_FragColor.rgb += smokeSource.z * max(10.0 - dist, 0.0);\\n\\n      //Smoke diffuse\\n      vec4 rightColor = texture2D(bufferTexture,vec2(pixel.x + 1.0/res.x,pixel.y));\\n      vec4 leftColor = texture2D(bufferTexture,vec2(pixel.x - 1.0/res.x,pixel.y));\\n      vec4 upColor = texture2D(bufferTexture,vec2(pixel.x,pixel.y + 1.0/res.y));\\n      vec4 downColor = texture2D(bufferTexture,vec2(pixel.x,pixel.y - 1.0/res.y));\\n\\n      //Diffuse equation\\n      float factor = 8.0 * 0.016 * (leftColor.r + rightColor.r + downColor.r * 3.0 + upColor.r - 6.0 * gl_FragColor.r);\\n\\n      //Account for low precision of texels\\n      float minimum = 0.000003;\\n      if (factor >= -minimum && factor < -0.10) factor = -minimum;\\n\\n      gl_FragColor.rgb += factor;\\n  }`\\n);\\n\\nextend({ OffScreenMaterial });\\n\\n\")), mdx(\"p\", null, \"Who ever came up with this shader is pretty clever! we are finding the distance between the uvs of the clicked position and the uvs passed from the vertex shader which is the current vertex/fragment, multiplied by the screen width and height.\"), mdx(\"p\", null, \"Then as the distance gets bigger we sort of invert it and use the z coordinate of the onKeyDown and onKeyUp as a multiplier. So as the dist gets bigger the value we multiply the z actually gets smaller and if it gets to small we just 0 it with the max function.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-glsl\"\n  }, \"float dist = distance(\\n    // current uvs 0 - 1.0 range and the screen resolution\\n    // however big the screen is in pixels\\n    vec2(0.5, 0.5) * vec2(1024.0, 768.0), \\n    // Clicked position / smokeSource uvs and Resolution\\n    // again\\n    vec2(0.12, 0.89) * vec2(1024.0, 768.0)\\n);\\n\")), mdx(\"p\", null, \"When we click the z will be 0.1 and:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-glsl\"\n  }, \"gl_FragColor.rgb += smokeSource.z * max(10.0 - dist, 0.0);\\n\")), mdx(\"p\", null, \"The brightness of the colors increases as the rgb channels in a vec3 get to 1.0, i.e. vec3(1.0, 1.0, 1.0) is white and as the rgb channels go from 0.0-1.0 range, they go from black to white respectively. (not limited to this range though)\"), mdx(\"p\", null, \"This makes sense now, as when we click the z is 0.1 which will produce more white and when not clicked the z will be 0 and 0 multiplied by anything is 0 - which produces black when not clicked.\"), mdx(\"p\", null, \"The algorithm which produces the smoke effect:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-glsl\"\n  }, \"//Smoke diffuse\\nvec4 rightColor = texture2D(bufferTexture,vec2(pixel.x + 1.0/res.x,pixel.y));\\nvec4 leftColor = texture2D(bufferTexture,vec2(pixel.x - 1.0/res.x,pixel.y));\\nvec4 upColor = texture2D(bufferTexture,vec2(pixel.x,pixel.y + 1.0/res.y));\\nvec4 downColor = texture2D(bufferTexture,vec2(pixel.x,pixel.y - 1.0/res.y));\\n\\n//Diffuse equation\\nfloat factor = 8.0 * 0.016 * (leftColor.r + rightColor.r + downColor.r * 3.0 + upColor.r - 6.0 * gl_FragColor.r);\\n\\n//Account for low precision of texels\\nfloat minimum = 0.000003;\\nif (factor >= -minimum && factor < -0.10) factor = -minimum;\\n\\ngl_FragColor.rgb += factor;\\n\")), mdx(\"p\", null, \"I don\\u2019t understand and being honest isn\\u2019t really the purpose of this article - the purpose being to show you how to utilize ping pong textures and a workflow from shadertoy to R3F.\"), mdx(\"p\", null, \"Until next time! Im going to use this to port a shader in the near future!\"));\n}\n;\nMDXContent.isMDXComponent = true;","hero":{"full":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABsklEQVQ4y82SO48BARDH15vCI97iFYmCWmgoNBKCwisSEdFJRIJKQkLoEL1WTaJQ+AC+gUai4pP8LzN7ZM+5K666YrKz8/jtzPxXEAQB70wulyMYDCKXy6FQKCCVSsFkMkEmk8FsNsNoNMJqtcLpdCIQCMButz963wPJfD4fer0eJpMJQw0GA9xuN2q1GrLZLJLJJMcHgwFarRY8Hs/vQCog4Gw2Q7lchl6vRyQSwWq1Qr/fRzqdRrPZxGazwX6/R6PREIEOhwPhcJgnoJUeQJ1Ox4BKpYJischrEni73aLdbsPv9/OUBDydTphOpyKwXq9jvV5zIB6Pf5nSYrHw/UqlElwuF093vV55TYVCgWg0it1uh+PxyByBguPxGOfzGbfbjRvUavUTSIcnYarVKmw2G5bLJS6XCzqdDudJuPl8jtFoBK/XK06Yz+dxOBxwv9+xWCygVCqfQPJpbRJCpVLxBt1uF6FQiPP0cToXKf15LjGYSCQwHA4Ri8W+iUO3ouOTT01arfZHIQWpCBqN5ikKPR8+rZ3JZJ5xaf7VhNdmqS8FSN/f1Uvqfv4P/2j/HPgBOApp0n0qxbsAAAAASUVORK5CYII=","aspectRatio":1.4842767295597483,"src":"/static/2744b98eaf1520802b029172689763a5/a1946/workflow-from-shadertoy-to-r3f.png","srcSet":"/static/2744b98eaf1520802b029172689763a5/5b37e/workflow-from-shadertoy-to-r3f.png 236w,\n/static/2744b98eaf1520802b029172689763a5/49058/workflow-from-shadertoy-to-r3f.png 472w,\n/static/2744b98eaf1520802b029172689763a5/a1946/workflow-from-shadertoy-to-r3f.png 944w,\n/static/2744b98eaf1520802b029172689763a5/030f1/workflow-from-shadertoy-to-r3f.png 1416w,\n/static/2744b98eaf1520802b029172689763a5/5baf2/workflow-from-shadertoy-to-r3f.png 1645w","srcWebp":"/static/2744b98eaf1520802b029172689763a5/99fbb/workflow-from-shadertoy-to-r3f.webp","srcSetWebp":"/static/2744b98eaf1520802b029172689763a5/77392/workflow-from-shadertoy-to-r3f.webp 236w,\n/static/2744b98eaf1520802b029172689763a5/1f177/workflow-from-shadertoy-to-r3f.webp 472w,\n/static/2744b98eaf1520802b029172689763a5/99fbb/workflow-from-shadertoy-to-r3f.webp 944w,\n/static/2744b98eaf1520802b029172689763a5/4a492/workflow-from-shadertoy-to-r3f.webp 1416w,\n/static/2744b98eaf1520802b029172689763a5/76fe6/workflow-from-shadertoy-to-r3f.webp 1645w","sizes":"(max-width: 944px) 100vw, 944px"},"regular":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABsklEQVQ4y82SO48BARDH15vCI97iFYmCWmgoNBKCwisSEdFJRIJKQkLoEL1WTaJQ+AC+gUai4pP8LzN7ZM+5K666YrKz8/jtzPxXEAQB70wulyMYDCKXy6FQKCCVSsFkMkEmk8FsNsNoNMJqtcLpdCIQCMButz963wPJfD4fer0eJpMJQw0GA9xuN2q1GrLZLJLJJMcHgwFarRY8Hs/vQCog4Gw2Q7lchl6vRyQSwWq1Qr/fRzqdRrPZxGazwX6/R6PREIEOhwPhcJgnoJUeQJ1Ox4BKpYJischrEni73aLdbsPv9/OUBDydTphOpyKwXq9jvV5zIB6Pf5nSYrHw/UqlElwuF093vV55TYVCgWg0it1uh+PxyByBguPxGOfzGbfbjRvUavUTSIcnYarVKmw2G5bLJS6XCzqdDudJuPl8jtFoBK/XK06Yz+dxOBxwv9+xWCygVCqfQPJpbRJCpVLxBt1uF6FQiPP0cToXKf15LjGYSCQwHA4Ri8W+iUO3ouOTT01arfZHIQWpCBqN5ikKPR8+rZ3JZJ5xaf7VhNdmqS8FSN/f1Uvqfv4P/2j/HPgBOApp0n0qxbsAAAAASUVORK5CYII=","aspectRatio":1.481818181818182,"src":"/static/2744b98eaf1520802b029172689763a5/3ddd4/workflow-from-shadertoy-to-r3f.png","srcSet":"/static/2744b98eaf1520802b029172689763a5/078a8/workflow-from-shadertoy-to-r3f.png 163w,\n/static/2744b98eaf1520802b029172689763a5/e56da/workflow-from-shadertoy-to-r3f.png 327w,\n/static/2744b98eaf1520802b029172689763a5/3ddd4/workflow-from-shadertoy-to-r3f.png 653w,\n/static/2744b98eaf1520802b029172689763a5/c5cc7/workflow-from-shadertoy-to-r3f.png 980w,\n/static/2744b98eaf1520802b029172689763a5/eebd2/workflow-from-shadertoy-to-r3f.png 1306w,\n/static/2744b98eaf1520802b029172689763a5/5baf2/workflow-from-shadertoy-to-r3f.png 1645w","srcWebp":"/static/2744b98eaf1520802b029172689763a5/0acdf/workflow-from-shadertoy-to-r3f.webp","srcSetWebp":"/static/2744b98eaf1520802b029172689763a5/ac59e/workflow-from-shadertoy-to-r3f.webp 163w,\n/static/2744b98eaf1520802b029172689763a5/7660b/workflow-from-shadertoy-to-r3f.webp 327w,\n/static/2744b98eaf1520802b029172689763a5/0acdf/workflow-from-shadertoy-to-r3f.webp 653w,\n/static/2744b98eaf1520802b029172689763a5/75470/workflow-from-shadertoy-to-r3f.webp 980w,\n/static/2744b98eaf1520802b029172689763a5/68d47/workflow-from-shadertoy-to-r3f.webp 1306w,\n/static/2744b98eaf1520802b029172689763a5/76fe6/workflow-from-shadertoy-to-r3f.webp 1645w","sizes":"(max-width: 653px) 100vw, 653px"},"narrow":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAB1ElEQVQ4y62STatpcRTGN/JaZKKkFAMh8p5QxIAYoUhmGFCiKB/BUExMmEiZm6AoEyVl5jv4Hs9prXu30DndezpnsNr7/2/t336eZy1BEAR8VkajEalUCul0GtlsFl6vF1KpFBKJBAqFAnK5HBqNhkur1fL577efA9VqNcN6vR4ajQb8fj9kMhksFgvi8TjcbjdCoRASiQQqlQoikQiUSuXXQJVKhVKphPF4jMFggEAgwHeFQgHD4RDVahX5fB7dbhfL5RKj0QjRaBQC2SDZ70BSEw6H0Ww20Wq1GKjT6dDpdDCbzVAulxEMBlGv17Fer7Hf79Hv9yE4HA62RPasVusLlAAej4fVkGWbzYbVaoXdbsf9FEuxWMR2u8XhcGC1AjXT4Xg8YjKZwGAwPIAUPgFJhc/nQywWw/V6xe12Qy6X4x6yvdlsMJ1OOVOBAr1cLrjf7zifz3A6nQ8gTY5U1Wo1BtJAFosFq3S5XNxjt9vRbreRyWT+DMVsNnPop9MJ8/n8RaEIpbwoQzoTmOyLa0JZ6/V6HthjbSirZDLJq/AMo52jJ7mgAX21ES8lfvQMEYs2gO4oBpPJxO90Jy74c5/4LvzXX79X/256d/Fj4K8r/E59ANC3h361Di44AAAAAElFTkSuQmCC","aspectRatio":1.4805194805194806,"src":"/static/2744b98eaf1520802b029172689763a5/502b1/workflow-from-shadertoy-to-r3f.png","srcSet":"/static/2744b98eaf1520802b029172689763a5/f2e6d/workflow-from-shadertoy-to-r3f.png 114w,\n/static/2744b98eaf1520802b029172689763a5/4ddba/workflow-from-shadertoy-to-r3f.png 229w,\n/static/2744b98eaf1520802b029172689763a5/502b1/workflow-from-shadertoy-to-r3f.png 457w,\n/static/2744b98eaf1520802b029172689763a5/7ddc2/workflow-from-shadertoy-to-r3f.png 686w,\n/static/2744b98eaf1520802b029172689763a5/435bf/workflow-from-shadertoy-to-r3f.png 914w,\n/static/2744b98eaf1520802b029172689763a5/5baf2/workflow-from-shadertoy-to-r3f.png 1645w","srcWebp":"/static/2744b98eaf1520802b029172689763a5/15384/workflow-from-shadertoy-to-r3f.webp","srcSetWebp":"/static/2744b98eaf1520802b029172689763a5/31fce/workflow-from-shadertoy-to-r3f.webp 114w,\n/static/2744b98eaf1520802b029172689763a5/e3e25/workflow-from-shadertoy-to-r3f.webp 229w,\n/static/2744b98eaf1520802b029172689763a5/15384/workflow-from-shadertoy-to-r3f.webp 457w,\n/static/2744b98eaf1520802b029172689763a5/0258d/workflow-from-shadertoy-to-r3f.webp 686w,\n/static/2744b98eaf1520802b029172689763a5/64ea2/workflow-from-shadertoy-to-r3f.webp 914w,\n/static/2744b98eaf1520802b029172689763a5/76fe6/workflow-from-shadertoy-to-r3f.webp 1645w","sizes":"(max-width: 457px) 100vw, 457px"},"seo":{"src":"/static/2744b98eaf1520802b029172689763a5/6050d/workflow-from-shadertoy-to-r3f.png"}}},{"id":"db14f8d2-5822-5167-b6ba-dabcdef9d7ea","slug":"/creating-amazing-particle-effect-along-a-curve-in-react-three-fiber","secret":false,"title":"Creating amazing particle effect along a curve in React Three Fiber","author":"Rick","date":"August 2nd, 2023","dateForSEO":"2023-08-02T00:00:00.000Z","timeToRead":2,"excerpt":"Creating particles in @react-three/fiber and making them follow a curve. This could be used for a cool dashboard GUI or combined with soft particles to procedurally place smoke coming from something - to name but a few uses.","canonical_url":null,"subscription":true,"body":"var _excluded = [\"components\"];\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Creating amazing particle effect along a curve in React Three Fiber\",\n  \"author\": \"Rick\",\n  \"date\": \"2023-08-02T00:00:00.000Z\",\n  \"hero\": \"./images/particles-following-curve-hero.png\",\n  \"excerpt\": \"Creating particles in @react-three/fiber and making them follow a curve. This could be used for a cool dashboard GUI or combined with soft particles to procedurally place smoke coming from something - to name but a few uses.\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, _excluded);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"The idea I had was to connect boxes with particle streams, so after having done this cool little side project a friend showed me a website which shows an example of \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.blueyard.com/economics\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"this\"), \" \"), mdx(\"p\", null, mdx(\"span\", {\n    parentName: \"p\",\n    \"className\": \"gatsby-resp-image-wrapper\",\n    \"style\": {\n      \"position\": \"relative\",\n      \"display\": \"block\",\n      \"marginLeft\": \"auto\",\n      \"marginRight\": \"auto\",\n      \"maxWidth\": \"1466px\"\n    }\n  }, \"\\n      \", mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"gatsby-resp-image-background-image\",\n    \"style\": {\n      \"paddingBottom\": \"72.16916780354707%\",\n      \"position\": \"relative\",\n      \"bottom\": \"0\",\n      \"left\": \"0\",\n      \"backgroundImage\": \"url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAADPUlEQVQ4yyVTW28bZRD97L1671577fV6vVl7bdexG9sJcUKTpq1VSnBDLiBCL+olRQghIgUeChLwgpCAvvPAI5X4oYez64fRzu7OzDkzZ0ZIJRVlIaOcP0syFMmGJjv0JeiqC6VsI2rFCJsxhJCgyhbcSgdySWOsSbPgGG2oil3kC1mq8KcOWdKhK17xQ5YMlJhcEmWajKoXwHMDSCySx5UIlvuqbNNcaJIPXa4XZESOtGanQCVa7ktlApQrBZs8WbBoSWjrDmSzYCYV8TYqSgOmGsOQI777EKWi3RzNK+jLZYvBBotWWKhE9iZMJYLHNk0tYGGFpjK5DkcfwjdmqJpT2Fq/YCkUyaETrFGUmEkt+E4PFd1Dw82QhvvY6p7SPsY0fQzP6sAxI/j2Jotto24t0LAO0XJXiJ1jCE32UVGbRMvgEjGwt3Grc4yjra9wdvQ97i0ucXf3HNPxHezNTrA/O0cWH6HdmFOcISy1T3YDNK0HmIQ3EJG3RFRdot+4Qrf2FFntBQbBSyw3f8RB9gO6wTH60Qrt+iH64QW69VO07YdktCzYVY0Ju4tgaV2E9l2IRe937Cd/YpG+wzR6i6T6GKFzhM3wCWbt7zBqfYlR/DmmyQtsRpcEu8TIf4ZO9RESf4VpfIN+/QJNZxdZ/Qxi2HiGfvAEsbdCyznkgMfwjDEDDsj4ORk/R+J+VhTvVFeomXM4lYyMkkKUUXiFj7beY9x6hVnyDQsGXzB5n7NbFMEN+wBx9YTLOqZAKRWOmdjnfG/BUTOqW4WhhvzeYqsJkuAEy/E/6NU/Rezfh5jFXyOuP+BQd/FB+hOG4as1CzXlxdS4tDWoZZ8b0KG1ufwBDG5Cw9lhy4+wm/7KDi8pTIeWQGxw6L3GGe4N/sDx1r8MvEM2Pfj6CBaL5JdQc3toejP45oTjGBYd3W69wbxzzbldcFe5u2J9HMIzMqo1x3Z6jYv5f5zFawTWDmw9hUXzvQmyDe7g4Ap7g7fY6/5MIa4pzjmBB1BVDZqmQ9EdHoUOoStucSWWtoFZ51vc7/+Fw8Fv2EluMI7eYDl5hw9Hv+CT2d843XlPZleFcDnz/N41xYCi8FQVs7is/wE18HSJ13n47wAAAABJRU5ErkJggg==')\",\n      \"backgroundSize\": \"cover\",\n      \"display\": \"block\"\n    }\n  }), \"\\n  \", mdx(\"picture\", {\n    parentName: \"span\"\n  }, \"\\n          \", mdx(\"source\", {\n    parentName: \"picture\",\n    \"srcSet\": [\"/static/3a2e7c8506120b812436efd28f969854/4c912/blueyard-example.webp 1466w\"],\n    \"sizes\": \"(max-width: 1466px) 100vw, 1466px\",\n    \"type\": \"image/webp\"\n  }), \"\\n          \", mdx(\"source\", {\n    parentName: \"picture\",\n    \"srcSet\": [\"/static/3a2e7c8506120b812436efd28f969854/a8aa1/blueyard-example.png 1466w\"],\n    \"sizes\": \"(max-width: 1466px) 100vw, 1466px\",\n    \"type\": \"image/png\"\n  }), \"\\n          \", mdx(\"img\", {\n    parentName: \"picture\",\n    \"className\": \"gatsby-resp-image-image\",\n    \"src\": \"/static/3a2e7c8506120b812436efd28f969854/a8aa1/blueyard-example.png\",\n    \"alt\": \"blueYard website landing page example\",\n    \"title\": \"blueYard website landing page example\",\n    \"loading\": \"lazy\",\n    \"style\": {\n      \"width\": \"100%\",\n      \"height\": \"100%\",\n      \"margin\": \"0\",\n      \"verticalAlign\": \"middle\",\n      \"position\": \"absolute\",\n      \"top\": \"0\",\n      \"left\": \"0\"\n    }\n  }), \"\\n        \"), \"\\n    \")), mdx(\"p\", null, \"So the implementation is probably different but particles following a path is occurring here.\"), mdx(\"iframe\", {\n    src: \"https://codesandbox.io/embed/soft-shadows-forked-wpf9py?fontsize=14&hidenavigation=1&theme=dark&view=preview\",\n    style: {\n      \"width\": \"100%\",\n      \"height\": \"500px\",\n      \"border\": \"0\",\n      \"borderRadius\": \"4px\",\n      \"overflow\": \"hidden\",\n      \"maxWidth\": \"500px\"\n    },\n    title: \"Soft shadows (forked)\",\n    allow: \"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\",\n    sandbox: \"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n  }), mdx(\"h1\", {\n    \"id\": \"the-curve\"\n  }, \"The curve\"), mdx(\"p\", null, \"A curve in three can be a few things like bexier catmullrom etc and you can define it like below:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const points = [\\n    new THREE.Vector3(-3.4, -3.5, 0),\\n    new THREE.Vector3(1, 0, 0),\\n    new THREE.Vector3(6, 0, 0),\\n    new THREE.Vector3(8, 0, 0)\\n    // Add more points here (200 points in total)\\n]\\nconst curve = new THREE.CatmullRomCurve3(points)\\n\\nconst numPoints = count\\n\\nconst progresses = useRef(new Array(numPoints).fill(0).map(() => Math.random()))\\n\")), mdx(\"p\", null, \"Define the points in an array and use in the instantiation of the CatmullRomCurve3 class. The progress along this curve is in the range 0-1 and we are using \"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"Math.random()\\n\")), mdx(\"p\", null, \"As we want the particles to start along the curve at different positions and then progress over time resetting at 1.0 and starting at 0.0.\"), mdx(\"h2\", {\n    \"id\": \"defining-the-render-method-for-points-and-buffferattributes\"\n  }, \"Defining the render method for points and buffferAttributes\"), mdx(\"p\", null, \"Below is an example of how to setup points with a custom shader and custom bufferAttributes.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"<group>\\n    <mesh\\n        position={[-5, -3.5, 0]}\\n        onPointerMove={(e) => {\\n            console.log(\\\"hello\\\")\\n            uniforms.mousePos.value.x = e.point.x\\n            uniforms.mousePos.value.y = e.point.y\\n        }}\\n        onPointerOut={() => {\\n            uniforms.mousePos.value.x = -150\\n            uniforms.mousePos.value.y = -150\\n        }}\\n    >\\n        <planeGeometry args={[100, 100]} />\\n        <meshBasicMaterial color={\\\"#28282b\\\"} />\\n    </mesh>\\n    <points ref={pointsRef}>\\n        <bufferGeometry attach=\\\"geometry\\\">\\n            <bufferAttribute\\n                attachObject={[\\\"attributes\\\", \\\"position\\\"]}\\n                array={new Float32Array(numPoints * 3)}\\n                itemSize={3}\\n                onUpdate={(self) => (self.needsUpdate = true)}\\n            />\\n            <bufferAttribute\\n                attachObject={[\\\"attributes\\\", \\\"vScale\\\"]}\\n                array={new Float32Array(numPoints)}\\n                itemSize={1}\\n                onUpdate={(self) => (self.needsUpdate = true)}\\n            />\\n        </bufferGeometry>\\n        <shaderMaterial \\n            ref={shaderMaterial} \\n            transparent \\n            depthWite={false} \\n            fragmentShader={fragmentShader} \\n            vertexShader={vertexShader} \\n            uniforms={uniforms} \\n        />\\n    </points>\\n</group>\\n\")), mdx(\"p\", null, \"This is pretty self explanatory. One small point to make is that we are using a plane position behind the points as a selector for our touch events as if we use the points directly you can get some weird artefact\\u2019s where you push the push with repulsion and then it bounces back.\"), mdx(\"h2\", {\n    \"id\": \"the-shaders-for-the-custom-points-material\"\n  }, \"The Shaders for the custom points material\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const vertexShader = `\\n    uniform float uPixelRatio;\\n    uniform float uSize;\\n    uniform float time;\\n    uniform vec3 mousePos;\\n\\n    attribute float vScale;\\n\\n    void main() {\\n        vec3 tempPos = vec3(position.xyz);\\n        \\n        vec3 seg = position - mousePos;\\n        vec3 dir = normalize(seg);\\n        float dist = length(seg);\\n        if (dist < 30.){\\n            float force = clamp(1. / (dist * dist), 0., 1.);\\n            tempPos += dir * force * 1.1;\\n        }\\n\\n        vec4 modelPosition = modelMatrix * vec4(tempPos, 1.);\\n        vec4 viewPosition = viewMatrix * modelPosition;\\n        vec4 projectionPosition = projectionMatrix * viewPosition;\\n\\n        \\n\\n        gl_Position = projectionPosition;\\n        gl_PointSize = uSize * vScale * uPixelRatio;\\n        gl_PointSize *= (1.0 / -viewPosition.z);\\n    }`\\n\\nconst fragmentShader = `\\nvoid main() {\\n  float _radius = 0.4;\\n  vec2 dist = gl_PointCoord-vec2(0.5);\\n  float strength = 1.-smoothstep(\\n    _radius-(_radius*0.4),\\n        _radius+(_radius*0.3),\\n        dot(dist,dist)*4.0\\n    );\\n\\n  gl_FragColor = vec4(0.2, 0.2, 6., strength);\\n}\\n`\\n\")), mdx(\"p\", null, \"The vertex shader is where the repulsion happens based on a pointer event in R3F and the idea came from \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://stackoverflow.com/questions/56370030/three-js-particle-repulsion-in-all-axes-xyz\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"this\"), \" stackOverflow answer. \"), mdx(\"p\", null, \"The Fragment shader just uses opacity to turn the square points into circles using opacity or strength. Its a simple SDF excluding anything outside of a circle.\"), mdx(\"h2\", {\n    \"id\": \"how-to-update-the-progress-along-the-curve\"\n  }, \"How to update the progress along the curve\"), mdx(\"p\", null, \"First of we want a random scale attribute for the points so that each point is scaled different, random is natural and better!\"), mdx(\"p\", null, \"Secondly we have positions and progresses float32 array which will be used to store positions and progress of each point.\"), mdx(\"p\", null, \"These can be accessed like so:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \" const updatedPositions = new Float32Array(numPoints * 3)\\n\\n updatedPositions[i] = ...\\n\")), mdx(\"p\", null, \"We are going to update this on the CPU as we aren\\u2019t talking about alot of particles and we need to use the threejs api which we cant really do in glsl shaders.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"useFrame(({ clock }) => {\\n    const updatedPositions = new Float32Array(numPoints * 3)\\n    const vScale = new Float32Array(numPoints)\\n\\n    const pointOnCurve = new THREE.Vector3()\\n\\n    for (let i = 0; i < numPoints; i++) {\\n      progresses.current[i] += 0.0006 // Adjust the animation speed here (smaller value for slower animation)\\n\\n      if (progresses.current[i] >= 1) {\\n        progresses.current[i] = 0 // Reset progress to 0 when it reaches 1\\n      }\\n\\n      const progress = progresses.current[i]\\n\\n      curve.getPointAt(progress, pointOnCurve)\\n\\n      updatedPositions[i * 3] = pointOnCurve.x + offset[i * 3]\\n      updatedPositions[i * 3 + 1] = pointOnCurve.y + offset[i * 3 + 1]\\n      updatedPositions[i * 3 + 2] = pointOnCurve.z + offset[i * 3 + 1]\\n\\n      vScale[i] = Math.random() * 1.1\\n    }\\n\\n    if (pointsRef.current && pointsRef.current.geometry.attributes && pointsRef.current.geometry.attributes.position) {\\n      pointsRef.current.geometry.attributes.position.array = updatedPositions\\n      pointsRef.current.geometry.attributes.vScale.array = vScale\\n      pointsRef.current.geometry.attributes.position.needsUpdate = true\\n\\n      shaderMaterial.current.uniforms.time.value = clock.elapsedTime\\n    }\\n  })\\n\")), mdx(\"p\", null, \"We loop over each point and increase the progress array of each particle in this loop. \"), mdx(\"p\", null, \"Then theres a conditional where by if we go over 1.0 of progress (the max) we set the progress to zero again.\"), mdx(\"p\", null, \"After which we can get a point on the curve by using this modified progress and have a vec3 (pointOnCurve) which stores it temporarily:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const progress = progresses.current[i]\\n\\ncurve.getPointAt(progress, pointOnCurve)\\n\")), mdx(\"p\", null, \"getPointAt is part of the threejs API and theres a few others aswell, go take alook!\"), mdx(\"p\", null, \"We can then use this point and an offset to update the positions:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"updatedPositions[i * 3] = pointOnCurve.x + offset[i * 3]\\nupdatedPositions[i * 3 + 1] = pointOnCurve.y + offset[i * 3 + 1]\\nupdatedPositions[i * 3 + 2] = pointOnCurve.z + offset[i * 3 + 1]\\n\")), mdx(\"p\", null, \"Why do we need an offset?\"), mdx(\"p\", null, \"without offset:\"), mdx(\"p\", null, mdx(\"span\", {\n    parentName: \"p\",\n    \"className\": \"gatsby-resp-image-wrapper\",\n    \"style\": {\n      \"position\": \"relative\",\n      \"display\": \"block\",\n      \"marginLeft\": \"auto\",\n      \"marginRight\": \"auto\",\n      \"maxWidth\": \"1466px\"\n    }\n  }, \"\\n      \", mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"gatsby-resp-image-background-image\",\n    \"style\": {\n      \"paddingBottom\": \"64.39290586630287%\",\n      \"position\": \"relative\",\n      \"bottom\": \"0\",\n      \"left\": \"0\",\n      \"backgroundImage\": \"url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAhUlEQVQ4y92T3QqAIAyFd5sEFflf+f5vuVwkGGi18KqLMc7Qj7NNQWuHLQP+AVTK3moWMF2mnINqUHgCXEH+oktQqLkyxqO1y1mjHGJsMdZDsx0Ow4RdJ3AcVYSHzJnnt0zuCAYAKER/6NIsX7WcDklpcJ511Oasu29L4T6TT1vmQJv/lB2iuEfoBy6Y5gAAAABJRU5ErkJggg==')\",\n      \"backgroundSize\": \"cover\",\n      \"display\": \"block\"\n    }\n  }), \"\\n  \", mdx(\"picture\", {\n    parentName: \"span\"\n  }, \"\\n          \", mdx(\"source\", {\n    parentName: \"picture\",\n    \"srcSet\": [\"/static/926d2f17ccb62f92132178e2f26ce0cc/4c912/without-offset-for-points.webp 1466w\"],\n    \"sizes\": \"(max-width: 1466px) 100vw, 1466px\",\n    \"type\": \"image/webp\"\n  }), \"\\n          \", mdx(\"source\", {\n    parentName: \"picture\",\n    \"srcSet\": [\"/static/926d2f17ccb62f92132178e2f26ce0cc/a8aa1/without-offset-for-points.png 1466w\"],\n    \"sizes\": \"(max-width: 1466px) 100vw, 1466px\",\n    \"type\": \"image/png\"\n  }), \"\\n          \", mdx(\"img\", {\n    parentName: \"picture\",\n    \"className\": \"gatsby-resp-image-image\",\n    \"src\": \"/static/926d2f17ccb62f92132178e2f26ce0cc/a8aa1/without-offset-for-points.png\",\n    \"alt\": \"example of points on curve without a constant offset\",\n    \"title\": \"example of points on curve without a constant offset\",\n    \"loading\": \"lazy\",\n    \"style\": {\n      \"width\": \"100%\",\n      \"height\": \"100%\",\n      \"margin\": \"0\",\n      \"verticalAlign\": \"middle\",\n      \"position\": \"absolute\",\n      \"top\": \"0\",\n      \"left\": \"0\"\n    }\n  }), \"\\n        \"), \"\\n    \")), mdx(\"p\", null, \"with offset:\"), mdx(\"p\", null, mdx(\"span\", {\n    parentName: \"p\",\n    \"className\": \"gatsby-resp-image-wrapper\",\n    \"style\": {\n      \"position\": \"relative\",\n      \"display\": \"block\",\n      \"marginLeft\": \"auto\",\n      \"marginRight\": \"auto\",\n      \"maxWidth\": \"1466px\"\n    }\n  }, \"\\n      \", mdx(\"span\", {\n    parentName: \"span\",\n    \"className\": \"gatsby-resp-image-background-image\",\n    \"style\": {\n      \"paddingBottom\": \"55.66166439290586%\",\n      \"position\": \"relative\",\n      \"bottom\": \"0\",\n      \"left\": \"0\",\n      \"backgroundImage\": \"url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAeElEQVQoz2MQE5P8T03MMCAGiopKYGUTZSBMg6gohA2iYeIIOQmcBuMxUAIFY5Mn2sswxeLiUlAxOSBWAGJZIJYHYinSXSgsLPafmZn5PwcHD9BgeZwuJdpAERFxoGGc/3l4eLEaQLaXQRgWMWQlG2yRQihmaZqwARo2FYUQfA50AAAAAElFTkSuQmCC')\",\n      \"backgroundSize\": \"cover\",\n      \"display\": \"block\"\n    }\n  }), \"\\n  \", mdx(\"picture\", {\n    parentName: \"span\"\n  }, \"\\n          \", mdx(\"source\", {\n    parentName: \"picture\",\n    \"srcSet\": [\"/static/64efd35f8b47e322ec9397c35db4beb5/4c912/particles-following-curve-hero.webp 1466w\"],\n    \"sizes\": \"(max-width: 1466px) 100vw, 1466px\",\n    \"type\": \"image/webp\"\n  }), \"\\n          \", mdx(\"source\", {\n    parentName: \"picture\",\n    \"srcSet\": [\"/static/64efd35f8b47e322ec9397c35db4beb5/a8aa1/particles-following-curve-hero.png 1466w\"],\n    \"sizes\": \"(max-width: 1466px) 100vw, 1466px\",\n    \"type\": \"image/png\"\n  }), \"\\n          \", mdx(\"img\", {\n    parentName: \"picture\",\n    \"className\": \"gatsby-resp-image-image\",\n    \"src\": \"/static/64efd35f8b47e322ec9397c35db4beb5/a8aa1/particles-following-curve-hero.png\",\n    \"alt\": \"example of points on curve with a constant offset\",\n    \"title\": \"example of points on curve with a constant offset\",\n    \"loading\": \"lazy\",\n    \"style\": {\n      \"width\": \"100%\",\n      \"height\": \"100%\",\n      \"margin\": \"0\",\n      \"verticalAlign\": \"middle\",\n      \"position\": \"absolute\",\n      \"top\": \"0\",\n      \"left\": \"0\"\n    }\n  }), \"\\n        \"), \"\\n    \")), mdx(\"p\", null, \"The offset is a constant offset for each point and does not change with progression along the curve. It widens the curve rather than having a single line of points:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const offset = new Float32Array(numPoints * 3)\\n\\nfor (let i = 0; i < numPoints; i++) {\\n    const xOffset = Math.random() * 0.29\\n    const yOffset = Math.random() * 0.29\\n    offset[i * 3] = xOffset\\n    offset[i * 3 + 1] = yOffset\\n    offset[i * 3 + 2] = 0\\n}\\n\")), mdx(\"iframe\", {\n    src: \"https://codesandbox.io/embed/soft-shadows-forked-wpf9py?fontsize=14&hidenavigation=1&theme=dark&view=preview\",\n    style: {\n      \"width\": \"100%\",\n      \"height\": \"500px\",\n      \"border\": \"0\",\n      \"borderRadius\": \"4px\",\n      \"overflow\": \"hidden\",\n      \"maxWidth\": \"500px\"\n    },\n    title: \"Soft shadows (forked)\",\n    allow: \"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\",\n    sandbox: \"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n  }));\n}\n;\nMDXContent.isMDXComponent = true;","hero":{"full":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAeElEQVQoz2MQE5P8T03MMCAGiopKYGUTZSBMg6gohA2iYeIIOQmcBuMxUAIFY5Mn2sswxeLiUlAxOSBWAGJZIJYHYinSXSgsLPafmZn5PwcHD9BgeZwuJdpAERFxoGGc/3l4eLEaQLaXQRgWMWQlG2yRQihmaZqwARo2FYUQfA50AAAAAElFTkSuQmCC","aspectRatio":1.8015267175572518,"src":"/static/64efd35f8b47e322ec9397c35db4beb5/a1946/particles-following-curve-hero.png","srcSet":"/static/64efd35f8b47e322ec9397c35db4beb5/5b37e/particles-following-curve-hero.png 236w,\n/static/64efd35f8b47e322ec9397c35db4beb5/49058/particles-following-curve-hero.png 472w,\n/static/64efd35f8b47e322ec9397c35db4beb5/a1946/particles-following-curve-hero.png 944w,\n/static/64efd35f8b47e322ec9397c35db4beb5/030f1/particles-following-curve-hero.png 1416w,\n/static/64efd35f8b47e322ec9397c35db4beb5/bf56b/particles-following-curve-hero.png 1466w","srcWebp":"/static/64efd35f8b47e322ec9397c35db4beb5/99fbb/particles-following-curve-hero.webp","srcSetWebp":"/static/64efd35f8b47e322ec9397c35db4beb5/77392/particles-following-curve-hero.webp 236w,\n/static/64efd35f8b47e322ec9397c35db4beb5/1f177/particles-following-curve-hero.webp 472w,\n/static/64efd35f8b47e322ec9397c35db4beb5/99fbb/particles-following-curve-hero.webp 944w,\n/static/64efd35f8b47e322ec9397c35db4beb5/4a492/particles-following-curve-hero.webp 1416w,\n/static/64efd35f8b47e322ec9397c35db4beb5/e58f4/particles-following-curve-hero.webp 1466w","sizes":"(max-width: 944px) 100vw, 944px"},"regular":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAeElEQVQoz2MQE5P8T03MMCAGiopKYGUTZSBMg6gohA2iYeIIOQmcBuMxUAIFY5Mn2sswxeLiUlAxOSBWAGJZIJYHYinSXSgsLPafmZn5PwcHD9BgeZwuJdpAERFxoGGc/3l4eLEaQLaXQRgWMWQlG2yRQihmaZqwARo2FYUQfA50AAAAAElFTkSuQmCC","aspectRatio":1.7912087912087913,"src":"/static/64efd35f8b47e322ec9397c35db4beb5/3ddd4/particles-following-curve-hero.png","srcSet":"/static/64efd35f8b47e322ec9397c35db4beb5/078a8/particles-following-curve-hero.png 163w,\n/static/64efd35f8b47e322ec9397c35db4beb5/e56da/particles-following-curve-hero.png 327w,\n/static/64efd35f8b47e322ec9397c35db4beb5/3ddd4/particles-following-curve-hero.png 653w,\n/static/64efd35f8b47e322ec9397c35db4beb5/c5cc7/particles-following-curve-hero.png 980w,\n/static/64efd35f8b47e322ec9397c35db4beb5/eebd2/particles-following-curve-hero.png 1306w,\n/static/64efd35f8b47e322ec9397c35db4beb5/bf56b/particles-following-curve-hero.png 1466w","srcWebp":"/static/64efd35f8b47e322ec9397c35db4beb5/0acdf/particles-following-curve-hero.webp","srcSetWebp":"/static/64efd35f8b47e322ec9397c35db4beb5/ac59e/particles-following-curve-hero.webp 163w,\n/static/64efd35f8b47e322ec9397c35db4beb5/7660b/particles-following-curve-hero.webp 327w,\n/static/64efd35f8b47e322ec9397c35db4beb5/0acdf/particles-following-curve-hero.webp 653w,\n/static/64efd35f8b47e322ec9397c35db4beb5/75470/particles-following-curve-hero.webp 980w,\n/static/64efd35f8b47e322ec9397c35db4beb5/68d47/particles-following-curve-hero.webp 1306w,\n/static/64efd35f8b47e322ec9397c35db4beb5/e58f4/particles-following-curve-hero.webp 1466w","sizes":"(max-width: 653px) 100vw, 653px"},"narrow":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAeElEQVQoz2MQE5P8T03MMCAGiopKYGUTZSBMg6gohA2iYeIIOQmcBuMxUAIFY5Mn2sswxeLiUlAxOSBWAGJZIJYHYinSXSgsLPafmZn5PwcHD9BgeZwuJdpAERFxoGGc/3l4eLEaQLaXQRgWMWQlG2yRQihmaZqwARo2FYUQfA50AAAAAElFTkSuQmCC","aspectRatio":1.8095238095238095,"src":"/static/64efd35f8b47e322ec9397c35db4beb5/502b1/particles-following-curve-hero.png","srcSet":"/static/64efd35f8b47e322ec9397c35db4beb5/f2e6d/particles-following-curve-hero.png 114w,\n/static/64efd35f8b47e322ec9397c35db4beb5/4ddba/particles-following-curve-hero.png 229w,\n/static/64efd35f8b47e322ec9397c35db4beb5/502b1/particles-following-curve-hero.png 457w,\n/static/64efd35f8b47e322ec9397c35db4beb5/7ddc2/particles-following-curve-hero.png 686w,\n/static/64efd35f8b47e322ec9397c35db4beb5/435bf/particles-following-curve-hero.png 914w,\n/static/64efd35f8b47e322ec9397c35db4beb5/bf56b/particles-following-curve-hero.png 1466w","srcWebp":"/static/64efd35f8b47e322ec9397c35db4beb5/15384/particles-following-curve-hero.webp","srcSetWebp":"/static/64efd35f8b47e322ec9397c35db4beb5/31fce/particles-following-curve-hero.webp 114w,\n/static/64efd35f8b47e322ec9397c35db4beb5/e3e25/particles-following-curve-hero.webp 229w,\n/static/64efd35f8b47e322ec9397c35db4beb5/15384/particles-following-curve-hero.webp 457w,\n/static/64efd35f8b47e322ec9397c35db4beb5/0258d/particles-following-curve-hero.webp 686w,\n/static/64efd35f8b47e322ec9397c35db4beb5/64ea2/particles-following-curve-hero.webp 914w,\n/static/64efd35f8b47e322ec9397c35db4beb5/e58f4/particles-following-curve-hero.webp 1466w","sizes":"(max-width: 457px) 100vw, 457px"},"seo":{"src":"/static/64efd35f8b47e322ec9397c35db4beb5/6050d/particles-following-curve-hero.png"}}},{"id":"a528cc9b-bea2-53ac-9149-5ae2f00588cb","slug":"/creating-realistic-water-ripples-a-beginner's-guide-to-reflective-effects","secret":false,"title":"Creating Realistic Water Ripples - A Beginner's Guide to Reflective Effects","author":"Rick","date":"June 3rd, 2023","dateForSEO":"2023-06-03T00:00:00.000Z","timeToRead":3,"excerpt":"This article provides a step-by-step tutorial for beginners, covering the basics of creating realistic reflective water ripples using shaders and reflective materials. Discover the techniques to bring your virtual water surfaces to life and add a basic reflective material.","canonical_url":null,"subscription":true,"body":"var _excluded = [\"components\"];\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Creating Realistic Water Ripples - A Beginner's Guide to Reflective Effects\",\n  \"author\": \"Rick\",\n  \"date\": \"2023-06-03T00:00:00.000Z\",\n  \"hero\": \"./images/reflective-water-base-hero.png\",\n  \"excerpt\": \"This article provides a step-by-step tutorial for beginners, covering the basics of creating realistic reflective water ripples using shaders and reflective materials. Discover the techniques to bring your virtual water surfaces to life and add a basic reflective material.\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, _excluded);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"In some respects this is a adaptation of another article about hdri\\u2019s and applying textures to an inner mesh, the article named \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.thefrontdev.co.uk/mastering-skybox-realism-loading-and-applying-hdri-with-three-and-r3f\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"Mastering Skybox Realism - Loading and Applying HDRI with Three and R3F\"), \".\"), mdx(\"iframe\", {\n    src: \"https://codesandbox.io/embed/friendly-sunset-66zz28?autoresize=1&fontsize=14&hidenavigation=1&theme=dark&view=preview\",\n    style: {\n      \"width\": \"100%\",\n      \"height\": \"500px\",\n      \"border\": \"0\",\n      \"borderRadius\": \"4px\",\n      \"overflow\": \"hidden\",\n      \"maxWidth\": \"500px\"\n    },\n    title: \"friendly-sunset-66zz28\",\n    allow: \"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\",\n    sandbox: \"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n  }), mdx(\"p\", null, \"This article allowed you to apply a hdri to a sphere and apply the light emitting aspects of the hdri to the objects in the scene.\"), mdx(\"p\", null, \"The extension to this is using: cube cameras, reflective materials from drei, a queue which stores center positions and radi for a circular SDF and a really cool customShaderMaterial from \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://github.com/FarazzShaikh/THREE-CustomShaderMaterial\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"Farazz\"), \" - which allows a developer to easily extend material classes from three or derivatives (meaning a class which extends of the base class materials from three).\"), mdx(\"p\", null, \"So I wanted a base to work off to create or mimic kelvin wakes. I here you say surely you can find something on shaderToy and port over? I found this to be quite complex and really hard, alittle beyond my reach as of now.\"), mdx(\"p\", null, \"However instead of a feedback loop for ever expanding elipsises to mimic kelvin wakes, can I not just use a queue of centers and radi of an SDF? I have done something similar in another article to do with vertex displacement. You have to be aware of hardware limitations ie max sizes of arrays, but we are way under with 300 and this is more than enough to implement this basic effect.\"), mdx(\"p\", null, \"The key take home message was that I can dynamically set a array in glsl with javascript template literals but its slow af. The reason being I couldnt get it to update with out causing rerenders which drastically reduces fps.\"), mdx(\"p\", null, \"Sometimes I think anything which has complex orchestration is better of doing programatically with threejs core api, rather than declaritvely.\"), mdx(\"p\", null, \"Personal opinion from 3-4 years of playing around with these kind of things and shader effects, Im not talking about basic shaderMaterials or posprocessing but things which have more than one render or off screen render targets.\"), mdx(\"h2\", {\n    \"id\": \"customshadermaterial\"\n  }, \"CustomShaderMaterial\"), mdx(\"p\", null, \"This is something really cool from Farazz created which allows easy extension of core materials or already exnted materials in R3F or Three.js.\"), mdx(\"p\", null, \"In my case I have extended MeshReflectiveMaterial from @react-three/drei (notice the import from the materials sub directory) this is because we cannot directly use a react component which this npm packaged exports from the main folder.\"), mdx(\"p\", null, \"CustomShaderMaterial can be defined like this:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"<CubeCamera>\\n  {(texture) => {\\n    return (\\n      <mesh\\n        rotation={[-Math.PI / 2, 0, 0]}\\n        scale={[200, 200, 200]}\\n        position={[0, 1, 0]}\\n        onPointerMove={(e) => {\\n          handleClick(e);\\n        }}\\n        ref={meshRef}\\n      >\\n        <planeGeometry />\\n        <CustomShaderMaterial\\n          ref={setMatRef}\\n          baseMaterial={MeshReflectorMaterial}\\n          vertexShader={patchShaders(customShader.vertex)}\\n          fragmentShader={patchShaders(customShader.fragment)}\\n          uniforms={uniforms}\\n          envMap={texture}\\n          metalness={1}\\n          roughness={0}\\n        />\\n      </mesh>\\n    );\\n  }}\\n</CubeCamera>\\n\")), mdx(\"p\", null, \"CustomShaderMaterial expects as a minimum:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"baseMaterial={MeshReflectorMaterial}\\nvertexShader={patchShaders(customShader.vertex)}\\nfragmentShader={patchShaders(customShader.fragment)}\\n\")), mdx(\"h2\", {\n    \"id\": \"cubecamera\"\n  }, \"CubeCamera\"), mdx(\"p\", null, \"The cubeCamera essentially takes a snap shot of the surroundings onto a 2d texture and used in the way stated above, this is a gross oversimplification, but im not trying to give a 101 on cubeCameras, just a general workflow or idea how to practicvally use them. This cube map will give us a texture we can use onto the surface of a mesh.\"), mdx(\"p\", null, \"So these 3 things are enabling the reflectivity:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"envMap={texture}\\nmetalness={1}\\nroughness={0}\\n\")), mdx(\"p\", null, \"We are using Pysically based material (PBR - which meshStandardMaterial and meshPhysicalMaterial) which means we can increase the metalness to give a good shine and decrease the roughness to give the appearance of less rough material and more smoothness.\"), mdx(\"h2\", {\n    \"id\": \"wake-and-dynamic-arrays-glsl\"\n  }, \"Wake and dynamic arrays GLSL\"), mdx(\"p\", null, \"I firstly attempted to recreate this shadertoy in r3f with a feedback loop for the different buffers, unfortunately I couldnt get this to work, each shadertoy seems to have different requirements with r3f setup.\"), mdx(\"p\", null, \"One day I should be able to come up with a solution to porting which is easy and effective, until then I will have to think of different avenues!\"), mdx(\"p\", null, \"The way I thought of, is to extend \", mdx(\"a\", {\n    parentName: \"p\",\n    \"href\": \"https://www.thefrontdev.co.uk/creating-water-trails-with-vertex-displacement-on-a-gltf-model\",\n    \"target\": \"_blank\",\n    \"rel\": \"noreferrer\"\n  }, \"this article\"), \".\"), mdx(\"p\", null, \"Essentially I tried to make a dyanmic array by using javascript template literals in the shaders.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-glsl\"\n  }, \"uniform vec2 positions[int(`${positions.current.length}`)]\\n\")), mdx(\"p\", null, \"Suffice to say this didnt work, I got to the stage where unless i had a state setting in onPointerMove and had the uniforms update based on this (which was slow), then I had to take another avenue.\"), mdx(\"p\", null, \"The over avenue was to have a fixed length to my array, and have a queue buffer ie, one in one out, this way the shader wouldnt complain and would compile.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"let positions = new Array(200).fill(new THREE.Vector2());\\nlet radius = new Array(200).fill(60);\\n\\n// more code\\n\\nuseFrame(() => {\\n  radius = radius.map((item, index) =>\\n    Math.min(item + 100 * easeCubic(index / 200), 60.0)\\n  );\\n\\n  if (matRef) {\\n    matRef.uniforms.radius.value = radius;\\n    matRef.uniforms.positions.value = positions;\\n  }\\n});\\n\")), mdx(\"p\", null, \"This shows a increase in radius, but the default radius have 60 so are at the maximum and dont animate, and positions have a default vec2.\"), mdx(\"p\", null, \"And the CustomShaderMaterial from Farazz allows easily updating of uniforms as it has the vertexShader and fragmentShader + uniforms on the material instance which normal materials dont have, which makes using onBeforeCompile really complicated.\"), mdx(\"p\", null, \"A neat solution which by all means is helping develop the shader ecosystem.\"), mdx(\"h2\", {\n    \"id\": \"the-main-shader\"\n  }, \"The main shader\"), mdx(\"p\", null, \"The shader is the summation of colors having looped through the positions array and using a sdf, creating a hollow circle with two ranges.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-glsl\"\n  }, \"vec3 colorSum = vec3(csm_DiffuseColor.rgb);\\nfor (int i = 0; i< MAX_SIZE; i++) {\\n  vec2 position = vUv; // Calculate the fragment's position in 2D\\n  vec2 center = positions[i]; // Set the center of the circle\\n  float radius = 1.0 / max(1.0, 60.0 - radius[i]);\\n\\n  float distance = circleSDF(position, center, radius);\\n\\n  if (distance < 0.0 && distance > -0.005) {\\n      // Fragment is inside the circle\\n      colorSum += colorSum * 0.06;\\n  } else {\\n      // Fragment is outside the circle\\n      colorSum += vec3(0.0, 0.0, 0.0); // Transparent color\\n  }\\n\\n  if (length(center) > 0.0) {\\n  }\\n  csm_DiffuseColor = vec4(colorSum, 1.0);\\n}\\n\")), mdx(\"p\", null, \"An important note to make is that using this library for a customShader you need to replace standard things like, gl_Position, gl_FragColor and three DiffuseColor with cms_FragColor or csm_DiffuseColor. And there are loads of ways to configure this so replacing specific bits of the shader etc etc.\"), mdx(\"h2\", {\n    \"id\": \"the-future-part-2\"\n  }, \"The future part 2\"), mdx(\"p\", null, \"This is a great base to work from and develop a kelvin wake algorithm along with reflective water, with a few coming tweaks this should be a cool water example. For now, have fun and watch out for the part 2 of this.\"));\n}\n;\nMDXContent.isMDXComponent = true;","hero":{"full":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAABYlAAAWJQFJUiTwAAAE1UlEQVQ4yx2U6VNTZxSHb51Olw+d6Tjtl44tdayttaKiSOs4VQSCLIqAEBMgZDEJ2SCEkARCFggkARLCIoLIEmTfsQ7dZ9ppp/3Saf+mpyf58Jv3vvPe+7zn/M45V7FYrdhcnVhdbkydXkzuAHpnD61mJ016G+U1Ddx7qOVWqYpPPv6IsxcKKS5vwGh2EYnEcDo9dLk8hIMREsNJlPrGBh7W11FdU01FZSXVtTWcP/cpp99/j3PnzvLu2woffHg6r7dOKbz5hsIpReHzz86ibtbQoNHz2OCgsuo+KlUlyvUrlygtucaly4WUlFznq8JCLhRe5eLVYi7fuMmNr7/hStF1iktuUiT7govFFHxZxJnzhXxxsZCia8Xc/PYuBQVneEcuV3TtelqMVlo6JE1nP62dETSuCE32MHXmfrS2fjnzU2cM8MDgp6LNj6q9nwpDhHpLGI0lgLMrgNXeg6fLg9LmDNBi96O2+mh84uVeWzd3NV3cVndyJ6fmnFzcaXJS9riL6lYP91q9PDb7qNT3c9cQpbYjzgNbAntwCsXZG8HsHkBtkRda3HlImcaNqqVbPvRQqZVnTQ7UzUNDbz7K+4Z+9I4BtI4o9R0xai1DVJtjqEwxlDq9hwptJ6USRanAVAKtbuuhTj5+ZPLn1Wj00WD002QJikI0WHJ2RMWWGI/swzTaRqi3xWmwJ1ByUdW2e6g3eqlp81CrE5i+l0YBaaziny0o0fej6QhhdA/R4Y3jHRjHFRhD4xxB25mgzZ1E60pi6kmhPDL7JRVvHlQlKdZIdA/04qcpkAepLQOYO/uw9sSw9Sbxh8eJJaZIZ+YYTT9jYmqBtGho9BmOvgmUcvGrQtstsFxkPpqkshpbiFZ7hHbXIMau4TzM7kvg8I8xlJjOw8ZSsywtrbK9ucXa6oasOywurqHUtos/pj6axZ82ZxSdQHROAbljWCU9U3eCVtcInoEUE5PPya68ZGN9k/n5ZTbWtjg6OGJzY0e0y872PopavNHYwtI2EpUrhqE7iasvRU94kvTMCtGxF9h8YwRjU6wsr3N8cMje7j7P5rP0xZ4y+XSF7w6P+f71Cb/+9DNKoznEA2NQJH51RIikswymVugOTTG/ss/x8Q9ks5usrW1ztC+wnYP8fnZO3glP0yLFsPdlCCUXmFvaQblvCJKT1jGEuSfB6NMNfLHn6N2jhMeWWN885OXqlgCyzMwus7u1x+H+EbvbB3gjM6h0YW6pg9zWhqk2DqM02wYxuOMCS9LVn5ZKZTB5J4hPLNM7OEs0OcfUzDKh4RnGMy9Ipp4zt7DOq+MTdF2j3Gruo7wtTLkuSo1JgCbPKL7odD5F/+CMzPOwwMdwBNK0d8YJDE4TiefaYym/ah0jcj4uz3M8kbVGRq9UG6KsNUyVfggl1zve6AyhxHweohOIzhWXQsXoCU0yEJslnlokmV4Sn6OoWgfEojATM6vs7b1mfDqLzZ9B353C4EmjDMQXSE5mycyuycGE/FWiNMhcah1xuSAjFZ7AH50lNZWV6BNU6UIy40G5ZIm///yL33/7Q7rhJbGxRabnNlGiYvzc4i4bW68kygW0zlHU9iRWX0b8eyHNnMHiTcmUiAVS0SopQpkmiDs4LR3wI//98y8nJ78wPL7Iyuo+/wO/B2ltmEG8XQAAAABJRU5ErkJggg==","aspectRatio":1.0260869565217392,"src":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/a1946/reflective-water-base-hero.png","srcSet":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/5b37e/reflective-water-base-hero.png 236w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/49058/reflective-water-base-hero.png 472w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/a1946/reflective-water-base-hero.png 944w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/e8f36/reflective-water-base-hero.png 986w","srcWebp":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/99fbb/reflective-water-base-hero.webp","srcSetWebp":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/77392/reflective-water-base-hero.webp 236w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/1f177/reflective-water-base-hero.webp 472w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/99fbb/reflective-water-base-hero.webp 944w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/e4622/reflective-water-base-hero.webp 986w","sizes":"(max-width: 944px) 100vw, 944px"},"regular":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAAFFElEQVQ4yy2U+VNTVxTH30yn0zpjpzpau0mntp2xtnbRatdpB2VTQAQRQkIgO4GsJISQlwVI8gKEhJ0QwKASlQi4dLedttMfOtPlr/r0QPvDmXvnzb2f+z3fc85T7H19OFwe7N4gNt8wJu8wVr+K0R2mw+bnis5KU4eFhqZW3n3/Q059cpEvWh2YnQEi6hgudwC3y08kHGVKm0QxGI0YTT3oDAau6bsxmK2c//g8p0+/w7lPPuXY0ed56+1TVJ04wTNPKRx4WuHgwQOcPXMGn9eP1eHB4vRjtjgx9VpRPj1/ls8/+oBTp07ywemTvHmiiqNHDnP4yBGOv1bFKy8f46UXX+D1116lquo4h44e47lDh3j+8GFOvPEm7505x2c1jfuPHnhWQbnSdIm29g6udploNw/QZvFx3TEkMUyLOYjREUBv9dJmGuSK0ceX7W6qO2Xt9NOgc9HaYcag76Gj/TqX6+tRrN4whoEwXa4Inf0RWmxhGnqHuGgIcEE/SK1End7PBYFc7PJzyRiktjtEozlCgyVOgz1Fd2CanuA0I6klAfqjWLwqnX0hOTxItc7LBd1/l2sFVCP7C50e6rp8XO4O0CSPNZrC2N0qXc4Yl6xj1Fr2Ypx6WwrF5FFpsw6JCp+k4hGQj3q52NgzRIspRHNPkCZjgKuyv26P0GZTabbEJJMEXe4UnW6NawNpWvs1mp0aSnWHQERVvajZi0YBXJHL16xh9H0qBqeod6gCU9E5E3S7x7EF0hi9adqcSVqdKdoHNIkMBl8W5ZptWA6HpRjiiyi5LB617ANH9kF70e2MYPGN4x6eIBjLMpqeJT21SCCeJ5iYIzQ2j0fNYxUflcsC2TO9Tszfh5mHBRahwxHF0C+KXNK8wQTOoTTWwAThRA5tcoHcTIGVYolbNze5ubHJxkaZ7Ow6Sp1+z+gQVy1hKUxMKp6gxz2G2ZsUVSlsgxlMPg1fJEs8Pc/cwiprazeZmStSKm3yYOcB5fIWd8oVKve2Udrt0f9Bo3IxRY8niVnWvqEJhkdncYZz4pcmqeYoFEpyqcLu9s4+rLh2m3t3t3m4+1DiEY8ffYOi6x+VJo5LpaPopWq+xBKqViCeKVC6vctK6T7jk8tMz62xdfc+jx8+prK1Q6F4G090VrybZe3GHba2HvLTD09Qms2q9JVKuyMhCtPMrO4QyaxhCWSZLVb47tsnbFd29tOpbG1zX/blzS1u3CiTzK5yXdqlvneU9v4JnJEFlBZrXGCjmP0ZPJEc4/lbuKOLDCYWGZ0qsVQss75eRsuuMDFdFFhFfHvEk+9/JDS+TLU+SrUhxlf6OHW94yi9vgzuSJ4B8SoQn6Xbo+EI5dFyN+gL5Yhry0zm10hOrjC3uEFI2iSRWeH25g72oRxf6lSBxqgxjlJvSqIMjOQZHltkJLlMIDEvRRFgMCtrRloli5pcFA+LzCxs4JazbfakzLxGNLVMNF3A5M/SJGNXK8BmaxolkFhATa+QmFyVtCfRyxjp+mXgXRpD8XlG5LH09DpTMxs0m8eoMURl5qNM5Ev88vOv3N/+mphWFDFL+OLycxifLrFQvMfy2tZ+qi22JG2OtKQ+hU+dxx7M4Y8tSNobMnLTNJkS1OhVyWqJ3375nX/+/JvNu4/EhlVW1isomdlbrN/cpSwfR1JFjDKPXa5J/PFFMrkSnpE58TKP0ZUR5RoNxrgAY1gGp9m4tSvAf/jrjz+ZL9yhsHqPfwHVbpgTElChAwAAAABJRU5ErkJggg==","aspectRatio":1.0251572327044025,"src":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/3ddd4/reflective-water-base-hero.png","srcSet":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/078a8/reflective-water-base-hero.png 163w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/e56da/reflective-water-base-hero.png 327w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/3ddd4/reflective-water-base-hero.png 653w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/c5cc7/reflective-water-base-hero.png 980w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/e8f36/reflective-water-base-hero.png 986w","srcWebp":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/0acdf/reflective-water-base-hero.webp","srcSetWebp":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/ac59e/reflective-water-base-hero.webp 163w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/7660b/reflective-water-base-hero.webp 327w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/0acdf/reflective-water-base-hero.webp 653w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/75470/reflective-water-base-hero.webp 980w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/e4622/reflective-water-base-hero.webp 986w","sizes":"(max-width: 653px) 100vw, 653px"},"narrow":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAABYlAAAWJQFJUiTwAAAE1UlEQVQ4yx2U6VNTZxSHb51Olw+d6Tjtl44tdayttaKiSOs4VQSCLIqAEBMgZDEJ2SCEkARCFggkARLCIoLIEmTfsQ7dZ9ppp/3Saf+mpyf58Jv3vvPe+7zn/M45V7FYrdhcnVhdbkydXkzuAHpnD61mJ016G+U1Ddx7qOVWqYpPPv6IsxcKKS5vwGh2EYnEcDo9dLk8hIMREsNJlPrGBh7W11FdU01FZSXVtTWcP/cpp99/j3PnzvLu2woffHg6r7dOKbz5hsIpReHzz86ibtbQoNHz2OCgsuo+KlUlyvUrlygtucaly4WUlFznq8JCLhRe5eLVYi7fuMmNr7/hStF1iktuUiT7govFFHxZxJnzhXxxsZCia8Xc/PYuBQVneEcuV3TtelqMVlo6JE1nP62dETSuCE32MHXmfrS2fjnzU2cM8MDgp6LNj6q9nwpDhHpLGI0lgLMrgNXeg6fLg9LmDNBi96O2+mh84uVeWzd3NV3cVndyJ6fmnFzcaXJS9riL6lYP91q9PDb7qNT3c9cQpbYjzgNbAntwCsXZG8HsHkBtkRda3HlImcaNqqVbPvRQqZVnTQ7UzUNDbz7K+4Z+9I4BtI4o9R0xai1DVJtjqEwxlDq9hwptJ6USRanAVAKtbuuhTj5+ZPLn1Wj00WD002QJikI0WHJ2RMWWGI/swzTaRqi3xWmwJ1ByUdW2e6g3eqlp81CrE5i+l0YBaaziny0o0fej6QhhdA/R4Y3jHRjHFRhD4xxB25mgzZ1E60pi6kmhPDL7JRVvHlQlKdZIdA/04qcpkAepLQOYO/uw9sSw9Sbxh8eJJaZIZ+YYTT9jYmqBtGho9BmOvgmUcvGrQtstsFxkPpqkshpbiFZ7hHbXIMau4TzM7kvg8I8xlJjOw8ZSsywtrbK9ucXa6oasOywurqHUtos/pj6axZ82ZxSdQHROAbljWCU9U3eCVtcInoEUE5PPya68ZGN9k/n5ZTbWtjg6OGJzY0e0y872PopavNHYwtI2EpUrhqE7iasvRU94kvTMCtGxF9h8YwRjU6wsr3N8cMje7j7P5rP0xZ4y+XSF7w6P+f71Cb/+9DNKoznEA2NQJH51RIikswymVugOTTG/ss/x8Q9ks5usrW1ztC+wnYP8fnZO3glP0yLFsPdlCCUXmFvaQblvCJKT1jGEuSfB6NMNfLHn6N2jhMeWWN885OXqlgCyzMwus7u1x+H+EbvbB3gjM6h0YW6pg9zWhqk2DqM02wYxuOMCS9LVn5ZKZTB5J4hPLNM7OEs0OcfUzDKh4RnGMy9Ipp4zt7DOq+MTdF2j3Gruo7wtTLkuSo1JgCbPKL7odD5F/+CMzPOwwMdwBNK0d8YJDE4TiefaYym/ah0jcj4uz3M8kbVGRq9UG6KsNUyVfggl1zve6AyhxHweohOIzhWXQsXoCU0yEJslnlokmV4Sn6OoWgfEojATM6vs7b1mfDqLzZ9B353C4EmjDMQXSE5mycyuycGE/FWiNMhcah1xuSAjFZ7AH50lNZWV6BNU6UIy40G5ZIm///yL33/7Q7rhJbGxRabnNlGiYvzc4i4bW68kygW0zlHU9iRWX0b8eyHNnMHiTcmUiAVS0SopQpkmiDs4LR3wI//98y8nJ78wPL7Iyuo+/wO/B2ltmEG8XQAAAABJRU5ErkJggg==","aspectRatio":1.027027027027027,"src":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/502b1/reflective-water-base-hero.png","srcSet":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/f2e6d/reflective-water-base-hero.png 114w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/4ddba/reflective-water-base-hero.png 229w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/502b1/reflective-water-base-hero.png 457w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/7ddc2/reflective-water-base-hero.png 686w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/435bf/reflective-water-base-hero.png 914w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/e8f36/reflective-water-base-hero.png 986w","srcWebp":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/15384/reflective-water-base-hero.webp","srcSetWebp":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/31fce/reflective-water-base-hero.webp 114w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/e3e25/reflective-water-base-hero.webp 229w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/15384/reflective-water-base-hero.webp 457w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/0258d/reflective-water-base-hero.webp 686w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/64ea2/reflective-water-base-hero.webp 914w,\n/static/a0a0b59cd4d3ddc71704981f32dfccbf/e4622/reflective-water-base-hero.webp 986w","sizes":"(max-width: 457px) 100vw, 457px"},"seo":{"src":"/static/a0a0b59cd4d3ddc71704981f32dfccbf/e8f36/reflective-water-base-hero.png"}}},{"id":"c7c16e02-e4d7-5a24-b4db-6fb4e0e1997a","slug":"/immersive-portals-in-three-and-r3f-unleashing-the-power-of-stencil-masks-from-drei","secret":false,"title":"Immersive Portals in Three and R3F - Unleashing the Power of Stencil Masks from Drei","author":"Rick","date":"May 21st, 2023","dateForSEO":"2023-05-21T00:00:00.000Z","timeToRead":2,"excerpt":"Dive into the world of portals in Three.js with the powerful stencil mask functionality from Drei. Experience seamless transitions and captivating environments as you traverse these enchanting gateways. With precise control over visibility using stencil masks, create immersive and interactive scenes that transport your audience to new dimensions. Discover the endless creative possibilities and unlock a realm of storytelling potential with portals in Three.js. Step into a world where reality and imagination converge, and embark on an immersive journey like never before.","canonical_url":null,"subscription":true,"body":"var _excluded = [\"components\"];\n\nfunction _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Immersive Portals in Three and R3F - Unleashing the Power of Stencil Masks from Drei\",\n  \"author\": \"Rick\",\n  \"date\": \"2023-05-21T00:00:00.000Z\",\n  \"hero\": \"./images/portal-effect-hero.png\",\n  \"excerpt\": \"Dive into the world of portals in Three.js with the powerful stencil mask functionality from Drei. Experience seamless transitions and captivating environments as you traverse these enchanting gateways. With precise control over visibility using stencil masks, create immersive and interactive scenes that transport your audience to new dimensions. Discover the endless creative possibilities and unlock a realm of storytelling potential with portals in Three.js. Step into a world where reality and imagination converge, and embark on an immersive journey like never before.\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, _excluded);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"The stencil masks provide a really powerful way of hiding parts of a scene behind mask. In this particular use case you have to fly through a circle to get to the other scene.\"), mdx(\"p\", null, \"You can make the scenes as big or as small as you like and so long as you define certain properties on the materials then everything will work seamlessly.\"), mdx(\"iframe\", {\n    src: \"https://codesandbox.io/embed/inverted-stencil-buffer-forked-gnj0h9?fontsize=14&hidenavigation=1&theme=dark&view=preview\",\n    style: {\n      \"width\": \"100%\",\n      \"height\": \"500px\",\n      \"border\": \"0\",\n      \"borderRadius\": \"4px\",\n      \"overflow\": \"hidden\",\n      \"maxWidth\": \"500px\"\n    },\n    title: \"Inverted stencil buffer (forked)\",\n    allow: \"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\",\n    sandbox: \"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n  }), mdx(\"p\", null, \"The first part is to do with the actual circle geometry which will represent our portal.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"<Mask id={1} position={[0, 0, 2.2]}>\\n  <circleGeometry args={[4.0, 128]} />\\n  <meshBasicMaterial\\n    colorWrite={true}\\n    depthWrite={false}\\n    stencilWrite={true}\\n    stencilRef={1}\\n    stencilFunc={THREE.AlwaysStencilFunc}\\n    stencilFail={THREE.ReplaceStencilOp}\\n    stencilZFail={THREE.ReplaceStencilOp}\\n    stencilZPass={THREE.ReplaceStencilOp}\\n    side={THREE.FrontSide}\\n    color={new THREE.Color(\\\"red\\\")}\\n  />\\n</Mask>\\n\")), mdx(\"p\", null, \"We use the mask component from @react-three/drei and wrap the geometry and the material in it. Its important you give it an id so we can determine what is masked and what is not, between worlds. The position is such that it is just infront of the atom.\"), mdx(\"p\", null, \"Be careful or the cameras near option and it doesnt look weird cutting things off or glitching, so set near to something really small. Not too precise as this can cause issues of its own.\"), mdx(\"p\", null, \"The matertial for the circle has colorwrite to true, I did this in particular as if I dont the background of the scene you are looking at through the portal isnt right, so if we are on the left and the left has a black bhackground, instead of the portal showing the red background it would be black as we set color write to false.\"), mdx(\"p\", null, \"These properties:\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"stencilRef={1}\\nstencilFunc={THREE.AlwaysStencilFunc}\\nstencilFail={THREE.ReplaceStencilOp}\\nstencilZFail={THREE.ReplaceStencilOp}\\nstencilZPass={THREE.ReplaceStencilOp}\\n\")), mdx(\"p\", null, \"I played around with until I found something which worked.\"), mdx(\"p\", null, \"Then i set it to frontside only so the portal trip would be one way! and the color to be red as the side we are on is black and the side we are looking at through the portal is red, this way we can have the seamless transition of background colors.\"), mdx(\"p\", null, \"You have to ensure the colors are the same of the circle material and the background of the opposite side for seamless transitions when we go through the circle.\"), mdx(\"p\", null, \"The atom is what we see and is the main mesh of the scene.\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"<Bounds fit clip observe>\\n  <Float floatIntensity={4} rotationIntensity={0} speed={4}>\\n    <Atom scale={1.5} />\\n  </Float>\\n</Bounds>\\n\")), mdx(\"p\", null, \"(FYI I forked this repo from another one which has these meshes on the codesandbox)\"), mdx(\"p\", null, \"And inside the Atom component we define a function which runs every time the camera changes position or rotation or in general changes one of its properties.\"), mdx(\"p\", null, \"This function controls how we invert the masks and scenes, such that it feels like we are entering two worlds.\"), mdx(\"p\", null, \"Quite simple but very effective!\"), mdx(\"pre\", null, mdx(\"code\", {\n    parentName: \"pre\",\n    \"className\": \"language-javascript\"\n  }, \"const hasCameraPassedThroughCircle = () => {\\n  const cameraPosition = camera.position.clone();\\n  const circleCenter = new THREE.Vector3(0, 0, 2.2);\\n  const cameraToCenter = circleCenter.clone().sub(cameraPosition);\\n  const distanceToCenter = cameraToCenter.length();\\n  const circleRadius = 4.0;\\n  if (distanceToCenter < circleRadius) {\\n    // Camera passes through the circle geometry\\n    return false;\\n    console.log(\\\"Camera passed through the portal.\\\");\\n  } else {\\n    // Camera does not pass through the circle geometry\\n    return true;\\n    console.log(\\\"Camera does not pass through the portal.\\\");\\n  }\\n};\\n\\nconst handleCameraMove = (event) => {\\n  // Create a vector representing the circle's normal\\n  const circleNormal = new THREE.Vector3(0, 0, 1);\\n\\n  // Create a vector representing the target point\\n  const targetPoint = camera.position;\\n\\n  // Create a vector representing the circle's position\\n  const circlePosition = new THREE.Vector3(0, 0, 2.2);\\n\\n  // Create a vector representing the direction from the circle's position to the target point\\n  const directionToTarget = targetPoint.clone().sub(circlePosition);\\n\\n  // Calculate the dot product of the direction vector and the circle normal\\n  const dotProduct = directionToTarget.dot(circleNormal);\\n\\n  if (!hasCameraPassedThroughCircle()) {\\n    if (dotProduct >= 0.0) {\\n      console.log(\\\"Target point is in front of the circle.\\\");\\n      ref.current.stencilWrite = true;\\n      ref.current.stencilRef = THREE.ReferenceStencilValue;\\n      ref.current.stencilFunc = THREE.NotEqualStencilFunc;\\n      ref.current.stencilFail = THREE.ReplaceStencilOp;\\n      ref.current.stencilZFail = THREE.ReplaceStencilOp;\\n      ref.current.stencilZPass = THREE.ReplaceStencilOp;\\n      scene.background = new THREE.Color(\\\"black\\\");\\n    } else {\\n      console.log(\\\"Target point is behind the circle.\\\");\\n      ref.current.stencilWrite = true;\\n      ref.current.stencilRef = 2;\\n      ref.current.stencilFunc = THREE.NotEqualStencilFunc;\\n      ref.current.stencilFail = THREE.ReplaceStencilOp;\\n      ref.current.stencilZFail = THREE.ReplaceStencilOp;\\n      ref.current.stencilZPass = THREE.ReplaceStencilOp;\\n      scene.background = new THREE.Color(\\\"red\\\");\\n    }\\n  }\\n};\\n\")), mdx(\"p\", null, \"The hasCameraPassedThroughCircle function checks if the camera has passed through the circle otherwise we dont change anything including the mask. If we have passed through the circle geometry we then check if we are infront or behind the circle mesh.\"), mdx(\"p\", null, \"If we are behind we invert the settings and have a red background and if we are infront we keep the main background black and this works seamlessly as we color the circle red which is the same as the inverted background.\"), mdx(\"p\", null, \"I havent properly inverted the effect so once you go through the other side will look red and as if the portal doesnt exist.\"), mdx(\"p\", null, \"You can image what kind of cool effects you can make with this workflow! and using something like drei\\u2019s helpers makes it so much easier.\"), mdx(\"p\", null, \"Not perfect but gives you the general idea :)\"));\n}\n;\nMDXContent.isMDXComponent = true;","hero":{"full":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAIAAAAmMtkJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAwElEQVQoz2NgoB1gYWBgYmBgJls/Bxl6gBYyMDJmsnM+4BWyYWaFihCrk4FBn4HxC6/gf0HxhVw8DMQ7npmREUjWaBv8V1T/zy2wl0cAIs5IlG5GRqDlLdOX34jJ+M/KuYtXkFjNjGBr+YWEJ245di0++z8r12KwzczE2QrWLCa5obrzu6T8f17BfA4uSLQRoRlMcrKwXuMR/M8r9IZfRI6JiYTQhrhwMhcvMKgL2TkZSEonEMsFmJitmVlZaJR4AbuzHtzK1nRxAAAAAElFTkSuQmCC","aspectRatio":1.5838926174496644,"src":"/static/1ae2508b35adcd227be8967b84af1464/7b7da/portal-effect-hero.png","srcSet":"/static/1ae2508b35adcd227be8967b84af1464/5b37e/portal-effect-hero.png 236w,\n/static/1ae2508b35adcd227be8967b84af1464/49058/portal-effect-hero.png 472w,\n/static/1ae2508b35adcd227be8967b84af1464/7b7da/portal-effect-hero.png 774w","srcWebp":"/static/1ae2508b35adcd227be8967b84af1464/a9a7d/portal-effect-hero.webp","srcSetWebp":"/static/1ae2508b35adcd227be8967b84af1464/77392/portal-effect-hero.webp 236w,\n/static/1ae2508b35adcd227be8967b84af1464/1f177/portal-effect-hero.webp 472w,\n/static/1ae2508b35adcd227be8967b84af1464/a9a7d/portal-effect-hero.webp 774w","sizes":"(max-width: 774px) 100vw, 774px"},"regular":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAIAAAAmMtkJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAwElEQVQoz2NgoB1gYWBgYmBgJls/Bxl6gBYyMDJmsnM+4BWyYWaFihCrk4FBn4HxC6/gf0HxhVw8DMQ7npmREUjWaBv8V1T/zy2wl0cAIs5IlG5GRqDlLdOX34jJ+M/KuYtXkFjNjGBr+YWEJ245di0++z8r12KwzczE2QrWLCa5obrzu6T8f17BfA4uSLQRoRlMcrKwXuMR/M8r9IZfRI6JiYTQhrhwMhcvMKgL2TkZSEonEMsFmJitmVlZaJR4AbuzHtzK1nRxAAAAAElFTkSuQmCC","aspectRatio":1.5825242718446602,"src":"/static/1ae2508b35adcd227be8967b84af1464/3ddd4/portal-effect-hero.png","srcSet":"/static/1ae2508b35adcd227be8967b84af1464/078a8/portal-effect-hero.png 163w,\n/static/1ae2508b35adcd227be8967b84af1464/e56da/portal-effect-hero.png 327w,\n/static/1ae2508b35adcd227be8967b84af1464/3ddd4/portal-effect-hero.png 653w,\n/static/1ae2508b35adcd227be8967b84af1464/7b7da/portal-effect-hero.png 774w","srcWebp":"/static/1ae2508b35adcd227be8967b84af1464/0acdf/portal-effect-hero.webp","srcSetWebp":"/static/1ae2508b35adcd227be8967b84af1464/ac59e/portal-effect-hero.webp 163w,\n/static/1ae2508b35adcd227be8967b84af1464/7660b/portal-effect-hero.webp 327w,\n/static/1ae2508b35adcd227be8967b84af1464/0acdf/portal-effect-hero.webp 653w,\n/static/1ae2508b35adcd227be8967b84af1464/a9a7d/portal-effect-hero.webp 774w","sizes":"(max-width: 653px) 100vw, 653px"},"narrow":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAIAAAAmMtkJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAwElEQVQoz2NgoB1gYWBgYmBgJls/Bxl6gBYyMDJmsnM+4BWyYWaFihCrk4FBn4HxC6/gf0HxhVw8DMQ7npmREUjWaBv8V1T/zy2wl0cAIs5IlG5GRqDlLdOX34jJ+M/KuYtXkFjNjGBr+YWEJ245di0++z8r12KwzczE2QrWLCa5obrzu6T8f17BfA4uSLQRoRlMcrKwXuMR/M8r9IZfRI6JiYTQhrhwMhcvMKgL2TkZSEonEMsFmJitmVlZaJR4AbuzHtzK1nRxAAAAAElFTkSuQmCC","aspectRatio":1.5833333333333333,"src":"/static/1ae2508b35adcd227be8967b84af1464/502b1/portal-effect-hero.png","srcSet":"/static/1ae2508b35adcd227be8967b84af1464/f2e6d/portal-effect-hero.png 114w,\n/static/1ae2508b35adcd227be8967b84af1464/4ddba/portal-effect-hero.png 229w,\n/static/1ae2508b35adcd227be8967b84af1464/502b1/portal-effect-hero.png 457w,\n/static/1ae2508b35adcd227be8967b84af1464/7ddc2/portal-effect-hero.png 686w,\n/static/1ae2508b35adcd227be8967b84af1464/7b7da/portal-effect-hero.png 774w","srcWebp":"/static/1ae2508b35adcd227be8967b84af1464/15384/portal-effect-hero.webp","srcSetWebp":"/static/1ae2508b35adcd227be8967b84af1464/31fce/portal-effect-hero.webp 114w,\n/static/1ae2508b35adcd227be8967b84af1464/e3e25/portal-effect-hero.webp 229w,\n/static/1ae2508b35adcd227be8967b84af1464/15384/portal-effect-hero.webp 457w,\n/static/1ae2508b35adcd227be8967b84af1464/0258d/portal-effect-hero.webp 686w,\n/static/1ae2508b35adcd227be8967b84af1464/a9a7d/portal-effect-hero.webp 774w","sizes":"(max-width: 457px) 100vw, 457px"},"seo":{"src":"/static/1ae2508b35adcd227be8967b84af1464/7b7da/portal-effect-hero.png"}}}],"pathPrefix":"/authors/rick","first":false,"last":false,"index":4,"pageCount":10,"additionalContext":{"author":{"authorsPage":true,"bio":"I am a creative frontend developer, I specialise in React, GLSL, postprocessing and R3F. I love to experiment with code and deal with complex topics.\n","id":"a2f54938-ce4a-58e3-a14f-42c21df2482a","name":"Rick","featured":true,"social":[{"url":"https://twitter.com/TheFrontDev"},{"url":"https://github.com/Richard-Thompson"}],"slug":"/authors/rick","avatar":{"small":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAaABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAIBAwQF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABRlsKyA51mANxhD//xAAcEAACAgIDAAAAAAAAAAAAAAABAgMSAAQREyD/2gAIAQEAAQUCCLCqVkRYwROt5aDCak7DFn2Dx3+P/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAHBAAAgMAAwEAAAAAAAAAAAAAAAECESEQIDEy/9oACAEBAAY/AnuH1fEU/KIuMUtQ0WyoYedP/8QAGxABAAIDAQEAAAAAAAAAAAAAAQARITFBUSD/2gAIAQEAAT8hv6qbzGnBGvZesblRLKajqgyHnSZETPIHoo4SmR6uJW1r7fx//9oADAMBAAIAAwAAABCQyAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/EB//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/EB//xAAeEAEAAgICAwEAAAAAAAAAAAABABEhMUFhIFGRsf/aAAgBAQABPxDOQFt9a7gbWsLru1ycRUEAjJbBWGActkvYxwhbwlRCKSwoC5sGwsd/kpeSkMZPRL0ptbPzw//Z","aspectRatio":0.7647058823529411,"src":"/static/80e6d82107a1db055d4e32d8c709c04c/fa1ea/Rick.jpg","srcSet":"/static/80e6d82107a1db055d4e32d8c709c04c/afb2b/Rick.jpg 13w,\n/static/80e6d82107a1db055d4e32d8c709c04c/7c20e/Rick.jpg 25w,\n/static/80e6d82107a1db055d4e32d8c709c04c/fa1ea/Rick.jpg 50w,\n/static/80e6d82107a1db055d4e32d8c709c04c/03612/Rick.jpg 75w,\n/static/80e6d82107a1db055d4e32d8c709c04c/61cdf/Rick.jpg 100w,\n/static/80e6d82107a1db055d4e32d8c709c04c/0ff54/Rick.jpg 1200w","srcWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/e7b2c/Rick.webp","srcSetWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/58718/Rick.webp 13w,\n/static/80e6d82107a1db055d4e32d8c709c04c/74aad/Rick.webp 25w,\n/static/80e6d82107a1db055d4e32d8c709c04c/e7b2c/Rick.webp 50w,\n/static/80e6d82107a1db055d4e32d8c709c04c/ed320/Rick.webp 75w,\n/static/80e6d82107a1db055d4e32d8c709c04c/66016/Rick.webp 100w,\n/static/80e6d82107a1db055d4e32d8c709c04c/9000d/Rick.webp 1200w","sizes":"(max-width: 50px) 100vw, 50px"},"medium":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAaABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAIBAwQF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABRlsKyA51mANxhD//xAAcEAACAgIDAAAAAAAAAAAAAAABAgMSAAQREyD/2gAIAQEAAQUCCLCqVkRYwROt5aDCak7DFn2Dx3+P/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAHBAAAgMAAwEAAAAAAAAAAAAAAAECESEQIDEy/9oACAEBAAY/AnuH1fEU/KIuMUtQ0WyoYedP/8QAGxABAAIDAQEAAAAAAAAAAAAAAQARITFBUSD/2gAIAQEAAT8hv6qbzGnBGvZesblRLKajqgyHnSZETPIHoo4SmR6uJW1r7fx//9oADAMBAAIAAwAAABCQyAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAEDAQE/EB//xAAUEQEAAAAAAAAAAAAAAAAAAAAg/9oACAECAQE/EB//xAAeEAEAAgICAwEAAAAAAAAAAAABABEhMUFhIFGRsf/aAAgBAQABPxDOQFt9a7gbWsLru1ycRUEAjJbBWGActkvYxwhbwlRCKSwoC5sGwsd/kpeSkMZPRL0ptbPzw//Z","aspectRatio":0.7575757575757576,"src":"/static/80e6d82107a1db055d4e32d8c709c04c/61cdf/Rick.jpg","srcSet":"/static/80e6d82107a1db055d4e32d8c709c04c/7c20e/Rick.jpg 25w,\n/static/80e6d82107a1db055d4e32d8c709c04c/fa1ea/Rick.jpg 50w,\n/static/80e6d82107a1db055d4e32d8c709c04c/61cdf/Rick.jpg 100w,\n/static/80e6d82107a1db055d4e32d8c709c04c/59538/Rick.jpg 150w,\n/static/80e6d82107a1db055d4e32d8c709c04c/fd013/Rick.jpg 200w,\n/static/80e6d82107a1db055d4e32d8c709c04c/0ff54/Rick.jpg 1200w","srcWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/66016/Rick.webp","srcSetWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/74aad/Rick.webp 25w,\n/static/80e6d82107a1db055d4e32d8c709c04c/e7b2c/Rick.webp 50w,\n/static/80e6d82107a1db055d4e32d8c709c04c/66016/Rick.webp 100w,\n/static/80e6d82107a1db055d4e32d8c709c04c/d9b14/Rick.webp 150w,\n/static/80e6d82107a1db055d4e32d8c709c04c/6b183/Rick.webp 200w,\n/static/80e6d82107a1db055d4e32d8c709c04c/9000d/Rick.webp 1200w","sizes":"(max-width: 100px) 100vw, 100px"},"large":{"base64":"data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAbABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAECAwQF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABg1aVCDnTzVmwxB//xAAcEAACAgMBAQAAAAAAAAAAAAABAgADBBESEzH/2gAIAQEAAQUCCLSqFbEWsEXr3aUEJ5JyGLPkHXvKxsn7P//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8BH//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8BH//EABsQAAICAwEAAAAAAAAAAAAAAAABAiEQETEy/9oACAEBAAY/AndHreIp8IuKStDRtmoUcLz/AP/EABwQAQACAgMBAAAAAAAAAAAAAAEAESFBEFFhMf/aAAgBAQABPyG/qJvMalEGO5aivuyVEsi17FYkyHWyJlDOoGoo0Sks2uN21r3cGsTBHvH/2gAMAwEAAgADAAAAEOAFMP/EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8QH//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8QH//EAB4QAQACAwACAwAAAAAAAAAAAAEAESExQRBhUXGR/9oACAEBAAE/EOgIHnWvcZWNF17tdORkgCmxgpDIO2JkNMCFprBCQ1LCqA/mCCCO4l8SkMZPgh3MbW7+TJQfdQg2hB4//9k=","aspectRatio":0.7522935779816514,"src":"/static/80e6d82107a1db055d4e32d8c709c04c/ec46e/Rick.jpg","srcSet":"/static/80e6d82107a1db055d4e32d8c709c04c/a2637/Rick.jpg 82w,\n/static/80e6d82107a1db055d4e32d8c709c04c/15203/Rick.jpg 164w,\n/static/80e6d82107a1db055d4e32d8c709c04c/ec46e/Rick.jpg 328w,\n/static/80e6d82107a1db055d4e32d8c709c04c/b69a5/Rick.jpg 492w,\n/static/80e6d82107a1db055d4e32d8c709c04c/23a36/Rick.jpg 656w,\n/static/80e6d82107a1db055d4e32d8c709c04c/0ff54/Rick.jpg 1200w","srcWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/5a48e/Rick.webp","srcSetWebp":"/static/80e6d82107a1db055d4e32d8c709c04c/2d087/Rick.webp 82w,\n/static/80e6d82107a1db055d4e32d8c709c04c/29d87/Rick.webp 164w,\n/static/80e6d82107a1db055d4e32d8c709c04c/5a48e/Rick.webp 328w,\n/static/80e6d82107a1db055d4e32d8c709c04c/42f2e/Rick.webp 492w,\n/static/80e6d82107a1db055d4e32d8c709c04c/dec03/Rick.webp 656w,\n/static/80e6d82107a1db055d4e32d8c709c04c/9000d/Rick.webp 1200w","sizes":"(max-width: 328px) 100vw, 328px"}}},"originalPath":"/authors/authors-rick","skip":6,"limit":6}}},"staticQueryHashes":["1143375668","1491088328","2444214635"]}