
Created 2025-09-14
/** * ============================================================================ * = Computer Graphics: Text-Based Shaders = * ============================================================================ * * This tutorial introduces fundamental concepts of computer graphics through * text-based shader implementations in Recho. * * I'm taking Prof. Ken Perlin's Computer Graphics course this semester. In the * second lecture, it's fascinating to see him creating impressive visual * effects with GPU-based fragment shaders. * * However, I find it challenging for beginners to set up WebGL and write GLSL * code. Therefore, I'm creating text-based shader implementations in Recho to * make computer graphics more accessible to beginners. * * > You don't have to be familiar with Canvas, WebGL, or GLSL. Only basic * > JavaScript knowledge and some basic vector operations are required. * * After reading this, you will be able to create the following rotating sphere * with diffuse reflection model and ambient light! */ //➜ //➜ +=~~~---::::....: //➜ *+==~~---::::.......... //➜ #**++==~~---::::..... .. //➜ %#*++===~~---::::..... . //➜ %%##*+++==~~~---::::...... .. //➜ %%%##**++===~~----::::....... ..: //➜ %%%%##**+++==~~~~---:::::.............: //➜ %%%%%##**+++===~~~----:::::...........: //➜ %%%%%%##**+++===~~~-----::::::......::: //➜ %%%%%%%##***++====~~~-----::::::::::::- //➜ %%%%%%%%##***+++====~~~~-------:::::--- //➜ %%%%%%%%%###***+++====~~~~~----------~~ //➜ %%%%%%%%%%%###***++++====~~~~~~~~~~~~~= //➜ %%%%%%%%%%%%###****++++=============+ //➜ %%%%%%%%%%%%%####****+++++++===+++* //➜ %%%%%%%%%%%%%%####*******+++*** //➜ %%%%%%%%%%%%%%%%%############ //➜ %%%%%%%%%%%%%%%%%%%%%%% //➜ %%%%%%%%%%%%%%%%% //➜ echo( shader((vPos) => { const theta = uTime / 400; const sqrt2 = Math.sqrt(2); const dx = Math.cos(theta) * sqrt2; const dz = Math.sin(theta) * sqrt2; const z2 = 1 - vPos.dot(vPos); const skyColor = new Vector3(0.5, 0.85, 1.5); const ambient = new Vector3(0.2, 0.1, 0.05); if (z2 > 0) { const dir = new Vector3(dx, 1, dz); const p = new Vector3(vPos.x, vPos.y, Math.sqrt(z2)); const intensity = 0.5 * Math.max(0, p.dot(dir)); const diffuse = skyColor.clone().multiplyScalar(intensity); const light = diffuse.add(ambient); return gray2ASCII(new Vector4(light.x, light.y, light.z, 1)); } return gray2ASCII(new Vector4(0, 0, 0, 0)); }), ); /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Shaders * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * Computer graphics involves rendering pixels on the screen. Shaders are * programs that control how pixels are rendered. Here we focus on fragment * shaders, which are programs that execute once per pixel on the screen, * determining the color of each pixel. * * Since Recho only supports text-based output, our fragment shaders are * functions that execute once per cell in the output string, determining the * character displayed in each cell. * * Below is our implementation. The `shader` function takes a `callback` * function and an optional `dimension` argument, returning the output string. * The `callback` function is evaluated for each cell in the output string, * receiving the cell's position as a `Vector2` object. The callback's * return value determines the character displayed in that cell. For each cell, * the position is normalized to the range of [-1, 1] from left to right, and * the range of [1, -1] from top to bottom. */ function shader(callback, [width, height] = [41, 21]) { let output = ""; for (let i = 0; i < height; i++) { const y = map(i, 0, height - 1, 1, -1); for (let j = 0; j < width; j++) { const x = map(j, 0, width - 1, -1, 1); const vPos = new Vector2(x, y); output += callback(vPos); } output += i === height - 1 ? "" : "\n"; } return output; } function map(x, d0, d1, r0, r1) { return r0 + ((r1 - r0) * (x - d0)) / (d1 - d0); } /** * Since JavaScript doesn't support vector operations, we need to use a * library to help us. Here we use the vector module in Three.js. We specify * the version to 0.160.0, because it's the last version that can be imported * by Recho. */ const {Vector2, Vector3, Vector4} = await recho.require("[email protected]/build/three.min.js"); /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Hello Circle! * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * Let's draw a circle to test the shader function. The circle is defined by * the equation `x^2 + y^2 = 1`. For each cell, we check if the distance from * the cell to the center of the circle is less than or equal to 1. If it is, * we draw a `+` character; otherwise, we draw a space character. Here is the * result: */ //➜ + //➜ +++++++++++++++++ //➜ +++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++++++ //➜ +++++++++++++++++++++++++ //➜ +++++++++++++++++ //➜ + echo(shader((vPos) => (vPos.length() <= 1 ? "+" : " "))); /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Colors to Characters * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * As mentioned above, actual fragment shaders primarily work with colors. * Therefore, it's better to focus on colors and convert them to characters * using simple rules in our text-based shader. A color is typically * represented by a `Vector4` object, containing the red, green, blue, and * alpha components. The alpha component is optional and is usually 1 for * opaque colors. * * Let's first implement a rule that converts the grayscale value of colors to * a sequence of ASCII characters. */ function gray2ASCII(color) { // If the color is transparent, return a space character. if (color.w !== undefined && color.w === 0) return " "; const chs = ["@", "%", "#", "*", "+", "=", "~", "-", ":", ".", " "]; const gray = (color.x + color.y + color.z) / 3; const i = Math.floor(gray * chs.length); return chs[i]; } /** * Then we can use the `gray2ASCII` function as shown below. This example maps * the x and y components of the position to the red and green components of * the color respectively, resulting in a gradient. */ //➜ ======+++++++++++***********###########%% //➜ ========+++++++++++***********########### //➜ ==========+++++++++++***********######### //➜ ~===========+++++++++++***********####### //➜ ~~~===========+++++++++++***********##### //➜ ~~~~~===========+++++++++++***********### //➜ ~~~~~~~===========+++++++++++***********# //➜ ~~~~~~~~~===========+++++++++++********** //➜ ~~~~~~~~~~~===========+++++++++++******** //➜ --~~~~~~~~~~~===========+++++++++++****** //➜ ----~~~~~~~~~~~===========+++++++++++**** //➜ ------~~~~~~~~~~~===========+++++++++++** //➜ --------~~~~~~~~~~~===========+++++++++++ //➜ ----------~~~~~~~~~~~===========+++++++++ //➜ :-----------~~~~~~~~~~~===========+++++++ //➜ :::-----------~~~~~~~~~~~===========+++++ //➜ :::::-----------~~~~~~~~~~~===========+++ //➜ :::::::-----------~~~~~~~~~~~===========+ //➜ :::::::::-----------~~~~~~~~~~~========== //➜ :::::::::::-----------~~~~~~~~~~~======== //➜ ..:::::::::::-----------~~~~~~~~~~~====== echo( shader((vPos) => { const rgb = new Vector3(vPos.x, vPos.y, 0).multiplyScalar(-0.5).addScalar(0.5); return gray2ASCII(rgb); }), ); /** * In addition to grayscale, we can also convert the RGB components of colors * to a sequence of emoji color blocks. */ function rgb2emoji(color) { if (color.w !== undefined && color.w === 0) return " "; const normalize = (x) => x.map((d) => d / 255); const emojis = [ {char: "🟥", rgb: normalize([196, 58, 38])}, {char: "🟧", rgb: normalize([240, 144, 54])}, {char: "🟨", rgb: normalize([234, 188, 64])}, {char: "🟩", rgb: normalize([81, 177, 52])}, {char: "🟦", rgb: normalize([40, 92, 233])}, {char: "🟪", rgb: normalize([173, 66, 246])}, {char: "⬛", rgb: normalize([3, 3, 3])}, {char: "⬜", rgb: normalize([217, 217, 217])}, ]; let best = emojis[0]; let bestDist = Infinity; for (const e of emojis) { const [r, g, b] = e.rgb; const {x, y, z} = color; const dist = (x - r) ** 2 + (y - g) ** 2 + (z - b) ** 2; if (dist < bestDist) { best = e; bestDist = dist; } } return best.char; } /** * However, the result is not optimal. Therefore, we'll use the grayscale rule * for the following examples. */ //➜ 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦⬛⬛⬛⬛⬛⬛⬛⬛ //➜ 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦🟦🟦🟦🟦⬛⬛⬛⬛ //➜ 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦🟦🟦🟦🟦🟦🟦🟦🟦⬛ //➜ 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦 //➜ 🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦 //➜ 🟧🟧🟧🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦🟦 //➜ 🟧🟧🟧🟧🟧🟧🟥🟥🟥🟥🟥🟥🟥🟩🟩🟩🟩🟦🟦🟦🟦🟦🟦🟦🟦 //➜ 🟧🟧🟧🟧🟧🟧🟧🟧🟧🟥🟥🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟦🟦🟦🟦 //➜ 🟧🟧🟧🟧🟧🟧🟧🟧🟧🟧🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟦 //➜ 🟧🟧🟧🟧🟧🟧🟧🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 //➜ 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 //➜ 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 //➜ 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 //➜ 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 //➜ 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 //➜ 🟨🟨🟨🟨🟨🟨🟨🟨⬜⬜⬜🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 //➜ 🟨🟨🟨🟨🟨🟨⬜⬜⬜⬜⬜🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 echo( shader( (vPos) => { const rgb = new Vector3(vPos.x, vPos.y, 0).multiplyScalar(-0.5).addScalar(0.5); return rgb2emoji(rgb); }, [25, 17], ), ); /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * First Sphere * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * Now let's draw a sphere. The sphere is defined by the equation `x^2 + y^2 + * z^2 = 1`. First, for each vPos (x, y), we compute the z component using * `1 - x^2 - y^2`, which is equivalent to `1 - vPos.dot(vPos)`. If z is * valid (z > 0), we compute the color of that cell; otherwise, we set the * color to transparent. * * We assign the z component to the red and green components of the color. * The larger the z value (closer to the center), the brighter the color * appears. This simulates a beam of light shining perpendicularly onto the * screen from outside. */ //➜ //➜ %%###*******###%% //➜ %##****+++++++++****##% //➜ %##**+++++=========+++++**##% //➜ ##**+++=================+++**## //➜ %#**+++=====~~~~~~~~~~~=====+++**#% //➜ %#**++====~~~~~~~~~~~~~~~~~====++**#% //➜ @#**++====~~~~~~~~~~~~~~~~~~~====++**#@ //➜ %#*++====~~~~~~---------~~~~~~====++*#% //➜ #**++===~~~~~~-----------~~~~~~===++**# //➜ #**++===~~~~~~-----------~~~~~~===++**# //➜ #**++===~~~~~~-----------~~~~~~===++**# //➜ %#*++====~~~~~~---------~~~~~~====++*#% //➜ @#**++====~~~~~~~~~~~~~~~~~~~====++**#@ //➜ %#**++====~~~~~~~~~~~~~~~~~====++**#% //➜ %#**+++=====~~~~~~~~~~~=====+++**#% //➜ ##**+++=================+++**## //➜ %##**+++++=========+++++**##% //➜ %##****+++++++++****##% //➜ %%###*******###%% //➜ echo( shader((vPos) => { const z2 = 1 - vPos.dot(vPos); if (z2 > 0) { const z = Math.sqrt(z2); return gray2ASCII(new Vector4(z, 0, z, 1)); } return gray2ASCII(new Vector4(0, 0, 0, 0)); }), ); /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Diffuse Reflection Model * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * Next, we use the diffuse reflection model to make the sphere more realistic. * Given a directional light source, the intensity of light on a surface is * proportional to the cosine of the angle between the light direction and the * surface normal. For a sphere, the surface normal at point (x, y, z) is the * normalized vector `(x, y, z)`. * * Here we define a light direction `dir` (1, 1, 1). For each point `p`, we use * the dot product between `p` and `dir` to compute the light intensity. */ //➜ //➜ *+===~~----:::::: //➜ #*++==~~~----:::::...:: //➜ @%#**++==~~~---:::::........: //➜ @%##**++==~~~----::::.........: //➜ @@%%##**++===~~~---:::::.........:: //➜ @@@@%##**+++==~~~~----:::::........:: //➜ @@@@@%%##**+++==~~~~----:::::::....:::- //➜ @@@@@%%###**+++===~~~-----::::::::::::- //➜ @@@@@@%%###**+++===~~~~------::::::::-- //➜ @@@@@@@%%###***+++===~~~~~------------~ //➜ @@@@@@@@@%%##***++++====~~~~~~------~~~ //➜ @@@@@@@@@@%%###***++++=====~~~~~~~~~~== //➜ @@@@@@@@@@@@%%###****+++++============+ //➜ @@@@@@@@@@@@@%%%###****+++++++++++++* //➜ @@@@@@@@@@@@@@%%%#####************# //➜ @@@@@@@@@@@@@@@%%%%############ //➜ @@@@@@@@@@@@@@@@@@%%%%%%%%%%@ //➜ @@@@@@@@@@@@@@@@@@@@@@@ //➜ @@@@@@@@@@@@@@@@@ //➜ echo( shader((vPos) => { const z2 = 1 - vPos.dot(vPos); if (z2 > 0) { const dir = new Vector3(1, 1, 1); const p = new Vector3(vPos.x, vPos.y, Math.sqrt(z2)); const intensity = 0.5 * Math.max(0, p.dot(dir)); return gray2ASCII(new Vector4(intensity, intensity, intensity, 1)); } return gray2ASCII(new Vector4(0, 0, 0, 0)); }), ); /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Ambient Light * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * To make the sphere appear smoother, we can add ambient lighting to the * sphere. Ambient light is constant lighting that is not affected by the * direction of the light source. * * Here we simply add the ambient light to the diffuse light to obtain the * final lighting. The sphere appears brighter, and the transitions between * layers are more natural. */ //➜ //➜ +=~~~---::::....: //➜ *+==~~---::::.......... //➜ #**++==~~---::::..... .. //➜ %#*++===~~---::::..... . //➜ %%##*+++==~~~---::::...... .. //➜ %%%##**++===~~----::::....... ..: //➜ %%%%##**+++==~~~~---:::::.............: //➜ %%%%%##**+++===~~~----:::::...........: //➜ %%%%%%##**+++===~~~-----::::::......::: //➜ %%%%%%%##***++====~~~-----::::::::::::- //➜ %%%%%%%%##***+++====~~~~-------:::::--- //➜ %%%%%%%%%###***+++====~~~~~----------~~ //➜ %%%%%%%%%%%###***++++====~~~~~~~~~~~~~= //➜ %%%%%%%%%%%%###****++++=============+ //➜ %%%%%%%%%%%%%####****+++++++===+++* //➜ %%%%%%%%%%%%%%####*******+++*** //➜ %%%%%%%%%%%%%%%%%############ //➜ %%%%%%%%%%%%%%%%%%%%%%% //➜ %%%%%%%%%%%%%%%%% //➜ echo( shader((vPos) => { const z2 = 1 - vPos.dot(vPos); const skyColor = new Vector3(0.5, 0.85, 1.5); const ambient = new Vector3(0.2, 0.1, 0.05); if (z2 > 0) { const dir = new Vector3(1, 1, 1); const p = new Vector3(vPos.x, vPos.y, Math.sqrt(z2)); const intensity = 0.5 * Math.max(0, p.dot(dir)); const diffuse = skyColor.clone().multiplyScalar(intensity); const light = diffuse.add(ambient); return gray2ASCII(new Vector4(light.x, light.y, light.z, 1)); } return gray2ASCII(new Vector4(0, 0, 0, 0)); }), ); /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Rotating Sphere * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * Next, we make the sphere rotate around the y-axis. We can use the time * variable to compute the direction of the light. The key is to change the `x` * and `z` components of the light direction. * * We define a time variable `uTime` using `recho.now()`, which is a generator * that yields the current time continuously. Every time the time variable * changes, the referencing block (shader) will be re-evaluated, resulting in * smooth animations. */ const uTime = recho.now(); //➜ //➜ ####*****+++==~~- //➜ %%%%%%%%%####***++==~~- //➜ %%%%%%%%%%%%%%%%###**+++==~-: //➜ %%%%%%%%%%%%%%%%%%%%##***++==~- //➜ %%%%%%%%%%%%%%%%%%%%%%%%##***++=~~: //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%##**++==~- //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##**+==~- //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##**++=~ //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##**+== //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#**+= //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##**+ //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##*+ //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#*+ //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#* //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%# //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% //➜ %%%%%%%%%%%%%%%%%%%%%%%%%%%%% //➜ %%%%%%%%%%%%%%%%%%%%%%% //➜ %%%%%%%%%%%%%%%%% //➜ echo( shader((vPos) => { const theta = uTime / 400; const sqrt2 = Math.sqrt(2); const dx = Math.cos(theta) * sqrt2; const dz = Math.sin(theta) * sqrt2; const z2 = 1 - vPos.dot(vPos); const skyColor = new Vector3(0.5, 0.85, 1.5); const ambient = new Vector3(0.2, 0.1, 0.05); if (z2 > 0) { const dir = new Vector3(dx, 1, dz); const p = new Vector3(vPos.x, vPos.y, Math.sqrt(z2)); const intensity = 0.5 * Math.max(0, p.dot(dir)); const diffuse = skyColor.clone().multiplyScalar(intensity); const light = diffuse.add(ambient); return gray2ASCII(new Vector4(light.x, light.y, light.z, 1)); } return gray2ASCII(new Vector4(0, 0, 0, 0)); }), ); /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Moving Sphere * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * Let's not only rotate the sphere, but also move it. This can be achieved by * updating the position and radius of the sphere. */ //➜ //➜ //➜ //➜ //➜ //➜ //➜ //➜ //➜ //➜ //➜ //➜ #*** //➜ %%%%%%%####*** //➜ %%%%%%%%%%%%%###*** //➜ %%%%%%%%%%%%%%%%###**+ //➜ %%%%%%%%%%%%%%%%%%%###** //➜ %%%%%%%%%%%%%%%%%%%%%%##** //➜ %%%%%%%%%%%%%%%%%%%%%%%##* //➜ %%%%%%%%%%%%%%%%%%%%%%%%#* //➜ %%%%%%%%%%%%%%%%%%%%%%%%# //➜ %%%%%%%%%%%%%%%%%%%%%%%# //➜ %%%%%%%%%%%%%%%%%%%%%% //➜ %%%%%%%%%%%%%%%%%% //➜ %%%%%%%%%%%% //➜ //➜ //➜ //➜ //➜ //➜ //➜ echo( shader( (vPos) => { const now = uTime / 2000; const theta = uTime / 400; const sqrt2 = Math.sqrt(2); const dx = Math.cos(theta) * sqrt2; const dz = Math.sin(theta) * sqrt2; const r = 0.3 + 0.2 * Math.sin(4 * now); const x = vPos.x + 0.5 * Math.sin(5 * now); const y = vPos.y + 0.5 * Math.sin(7 * now); const z2 = r * r - (x * x + y * y); const skyColor = new Vector3(0.5, 0.85, 1.5); const ambient = new Vector3(0.2, 0.1, 0.05); if (z2 > 0) { const dir = new Vector3(dx, 1, dz); const p = new Vector3(x, y, Math.sqrt(z2)); const intensity = 0.5 * Math.max(0, p.dot(dir)); const diffuse = skyColor.clone().multiplyScalar(intensity); const light = diffuse.add(ambient); return gray2ASCII(new Vector4(light.x, light.y, light.z, 1)); } return gray2ASCII(new Vector4(0, 0, 0, 0)); }, [61, 31], ), ); /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * More Spheres with more Lights * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * Our final example is more complex, featuring multiple spheres and multiple * lights. However, the concept remains the same as the previous examples. * The only difference is that for each cell, we need to compute the color for * each sphere, then select the one closest to the screen as the final color. */ //➜ //➜ ####***++ //➜ **###### ########*** //➜ ########### ############ //➜ ############# ########### //➜ ############# ######### //➜ ############# # //➜ ######### //➜ //➜ ####****+ //➜ ########*** //➜ ###########* //➜ ########### //➜ ######### //➜ ### //➜ %%*****+++= //➜ %%********+++ //➜ %%%***********+ //➜ %%%*********** //➜ %%%********* //➜ %%%*** ##***++ //➜ %%###****++ //➜ %%#######**** //➜ %%##########** //➜ *#****+++ %########### //➜ ######****+ %%####### //➜ #########*** //➜ ########### //➜ ########## //➜ ##### //➜ echo( shader( (vPos) => { const scale = 5; const now = uTime / 1000; const ambient = new Vector3(0.2, 0.1, 0.05); let fragColor = new Vector4(0, 0, 0, 0); let zMax = -1000; const L1 = new Vector3(Math.sin(now * 2), 1, 0).normalize(); const L2 = new Vector3(-1, -0.1, 0).normalize(); for (let i = 0; i < 10; i++) { const x = scale * vPos.x + 4 * Math.sin(11.8 * i + 100.3 + 0.3 * now); const y = scale * vPos.y + 4 * Math.sin(10.3 * i + 200.6 + 0.3 * now); const z = scale * vPos.y + 4 * Math.cos(10.3 * i + 200.6 + 0.3 * now); const r = 1 * 1 - (x * x + y * y); const z1 = z + r / (scale * scale); if (r > 0 && z1 > zMax) { zMax = z1; const p = new Vector3(x, y, Math.sqrt(r)); let D1 = p.clone().dot(L1); let D2 = p.clone().dot(L2); D1 = 0.4 * Math.max(0, D1 * Math.abs(D1)); D2 = 0.4 * Math.max(0, D1 * Math.abs(D2)); const d1 = new Vector3(0.2, 0.5, 1).multiplyScalar(D1); const d2 = new Vector3(0.2, 0.5, 1).multiplyScalar(D2); const diffuse = d1.add(d2); const color = new Vector3(0.5 + 0.5 * Math.sin(i), 0.5 + 0.5 * Math.sin(4 * i), 0.5 + 0.5 * Math.sin(5 * i)); const light = color.multiply(diffuse.add(ambient)); return gray2ASCII(new Vector4(Math.sqrt(light.x), Math.sqrt(light.y), Math.sqrt(light.z), 1)); } } return gray2ASCII(fragColor); }, [61, 31], ), ); /** * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Summary * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * * In this tutorial, we have learned how to use shaders to create basic * computer graphics effects. We have learned how to use the diffuse reflection * model to compute lighting, how to use ambient lighting to make the sphere * appear smoother, how to use the time variable to create animations, and how * to use multiple spheres and lights to create more complex effects. * * I hope you have enjoyed this tutorial. If you have any questions, please * feel free to comment on https://github.com/recho-dev/recho/issues/98. */