Skip to content
Tayakorn
← All experiments
LAB.08RunningStarted

Drawing an active volcano with equations alone

Continues LAB.06 Drawing a sea wave with equations alone

What was tried

Ran the equations Hamid Naderi Yeganeh published for his Active Volcano image and measured how close the result gets to his original, following LAB.06, which did the same with his Sea Wave. The equations are written on the artwork itself, so they were read off line by line under magnification and turned into a numpy script that renders 2000×1200 on the original grid. A pixel's colour is the sum of four terms — sky, mountain, lightning and cloud — and the cloud's texture and the pattern on the mountain come from sums of 50 layers of cosine waves. The playable version in this entry is a shader that runs the same equations on the GPU of whatever device opens the page: each term can be switched off, the difference that produces the shading can be turned and stretched, the steepness of the shading step can be changed, and the sums can be cut short. The original is his work and is not reproduced here; every image in this entry is drawn from these equations.

The full equations

Transcribed from the formulas Hamid Naderi Yeganeh printed on his Active Volcano image, in the image's own order from the pixel colour down to the smallest parts. Every function takes (x, y); the arguments are left out. A few are written in an easier form, such as (17/20)^s for 17^s/20^s; no value changes.

  1. pixel(m,n)=rgb(F(H0),F(H1),F(H2)),x=m1000600,y=601n600\operatorname{pixel}(m,n)=\operatorname{rgb}\big(F(H_0),\,F(H_1),\,F(H_2)\big),\qquad x=\frac{m-1000}{600},\quad y=\frac{601-n}{600}

    A 2000×1200 image · column m = 1…2000, row n = 1…1200 · the three channels are one equation at v = 0, 1, 2

  2. F(x)=255ee1000x|x|ee1000(x1)F(x)=\left\lfloor 255\,e^{-e^{-1000x}}\,|x|^{e^{-e^{1000(x-1)}}}\right\rfloor

    The same F as printed for LAB.06 — this entry uses it as is, in the script and in the playable version

  3. Hv=2v2v+1220(1W1)(1110x25+y40)(1+3v213v+220ee3y+94)(1R)+W1(v2v+240+349v+3v240ee1000(T1T0))(1R)+(250+175(v2v))exp(e30|xy7+(y12)225+arccos(cos(35y+6x+2cos(15y7x)))50+arccos(cos(145y34x+2cos(30x+18y)))250|+3320+3100(6y72)10)+9+3B120(12v50+3v28v+1110ee500(C1C0))R\begin{aligned}H_v={}&\frac{2v^2-v+12}{20}\left(1-W_1\right)\left(\frac{11}{10}-\frac{x}{25}+\frac{y}{40}\right)\left(1+\frac{3v^2-13v+2}{20}\,e^{-e^{3y+\frac{9}{4}}}\right)\left(1-R\right)\\&+W_1\left(\frac{v^2-v+2}{40}+\frac{34-9v+3v^2}{40}\,e^{-e^{1000\left(T_1-T_0\right)}}\right)\left(1-R\right)\\&+\left(250+175\left(v^2-v\right)\right)\exp\!\left(-e^{30\left|x-\frac{y}{7}+\left(y-\frac{1}{2}\right)^2-\frac{2}{5}+\frac{\arccos(\cos(35y+6x+2\cos(15y-7x)))}{50}+\frac{\arccos(\cos(145y-34x+2\cos(30x+18y)))}{250}\right|+\frac{33}{20}+\frac{3}{100}\left(6y-\frac{7}{2}\right)^{10}}\right)\\&+\frac{9+3B_1}{20}\left(\frac{12-v}{50}+\frac{3v^2-8v+11}{10}\,e^{-e^{-500\left(C_1-C_0\right)}}\right)R\end{aligned}

    Four terms, one per line: sky · mountain · lightning · cloud · W, T, C and B are only used at v = 0 and 1 and shared by all three channels · T₁ − T₀ and C₁ − C₀ are differences between two points a tiny step apart, which is the shading of the mountain and the cloud

  4. R=exp(e50(75+P1+12P14(Q1+P12)2A1B145)e1000(y+45x2))R=\exp\!\left(-e^{-50\left(\frac{7}{5}+P_1+\frac{1}{2}P_1^4-\left(Q_1+\frac{P_1}{2}\right)^2-A_1-\frac{B_1}{45}\right)}-e^{-1000\left(y+\frac{4}{5}-\frac{x}{2}\right)}\right)

    The cloud mask: 1 inside the cloud, 0 outside · the cloud's axis is the line Q + P/2 = 0, leaning left as it rises; the ragged edge comes from A and B

  5. Tv=s=150(1720)sexp(e510cos(120(65)s(cos(3s2)QvMv+2sin(3s2)Sv)+2cos(5s2))cos(120(65)s(sin(3s2)QvMv2cos(3s2)Sv)+2cos(7s2)))T_v=\sum_{s=1}^{50}\left(\frac{17}{20}\right)^s\exp\!\left(-e^{5-10\cos\left(\frac{1}{20}\left(\frac{6}{5}\right)^s\left(\cos(3s^2)\,Q_vM_v+2\sin(3s^2)\,S_v\right)+2\cos(5s^2)\right)\cos\left(\frac{1}{20}\left(\frac{6}{5}\right)^s\left(\sin(3s^2)\,Q_vM_v-2\cos(3s^2)\,S_v\right)+2\cos(7s^2)\right)}\right)

    The mountain's surface pattern — the image only ever shows the difference T₁ − T₀, never T itself

  6. Av=s=150(87100)s(Kv,sLv,s)4A_v=\sum_{s=1}^{50}\left(\frac{87}{100}\right)^s\left(K_{v,s}L_{v,s}\right)^4
  7. Cv=5|75+Pv+Pv42(Qv+Pv2)2Av|C_v=\sqrt{5\left|\frac{7}{5}+P_v+\frac{P_v^4}{2}-\left(Q_v+\frac{P_v}{2}\right)^2-A_v\right|}

    A pretend height for the cloud — the expression under the root is the one inside R (without B), so C₁ − C₀ gives shading that follows the cloud's shape

  8. Bv=s=1502(197200)s(310Kv,s6)(310Lv,s6)B_v=\sum_{s=1}^{50}2\left(\frac{197}{200}\right)^s\left(\frac{3}{10}-K_{v,s}^6\right)\left(\frac{3}{10}-L_{v,s}^6\right)
  9. Sv=Vv2+13Qv2S_v=\sqrt{V_v^2+\frac{1}{3}Q_v^2}
  10. Kv,s=cos(12(2320)s(cos(s2)Qv+32sin(s2)Pv)+12cos(32(2320)s(cos(2s2)Qv+32sin(2s2)Pv))+cos(7s2))K_{v,s}=\cos\left(\frac{1}{2}\left(\frac{23}{20}\right)^s\left(\cos(s^2)\,Q_v+\frac{3}{2}\sin(s^2)\,P_v\right)+\frac{1}{2}\cos\left(\frac{3}{2}\left(\frac{23}{20}\right)^s\left(\cos(2s^2)\,Q_v+\frac{3}{2}\sin(2s^2)\,P_v\right)\right)+\cos(7s^2)\right)

    K and L are cosine waves that turn every layer and get 15% finer each time — 50 layers of them make the cloud's texture

  11. Lv,s=cos(12(2320)s(32cos(s2)Pvsin(s2)Qv)+12cos(32(2320)s(cos(3s2)Qv+32sin(3s2)Pv))+cos(8s2))L_{v,s}=\cos\left(\frac{1}{2}\left(\frac{23}{20}\right)^s\left(\frac{3}{2}\cos(s^2)\,P_v-\sin(s^2)\,Q_v\right)+\frac{1}{2}\cos\left(\frac{3}{2}\left(\frac{23}{20}\right)^s\left(\cos(3s^2)\,Q_v+\frac{3}{2}\sin(3s^2)\,P_v\right)\right)+\cos(8s^2)\right)
  12. Wv=exp(e1000Vv1000(7+800|20Qv7|322032)18cos2(32Pv19Qvcos(12Pv+7Qv))4cos2(82Pv+49Qv2cos(32Pv27Qv))+400+1000023|Qv|25)W_v=\exp\!\left(-e^{1000V_v-1000\left(7+800\left|20Q_v-7\right|^{\frac{3}{2}}20^{-\frac{3}{2}}\right)^{-1}-8\cos^2\left(32P_v-19Q_v-\cos(12P_v+7Q_v)\right)-4\cos^2\left(82P_v+49Q_v-2\cos(32P_v-27Q_v)\right)+400+\frac{10000}{23}\left|Q_v\right|^{\frac{2}{5}}}\right)

    The mountain mask · |Q_v|^(2/5) makes the sharp summit at x = 0 · the term (7 + 800|20Q_v − 7|^(3/2) 20^(−3/2))^(−1) is the small second peak to the right, at x = 0.35

  13. Mv=5052(23|5Vv+2|)52M_v=50^{\frac{5}{2}}\left(23\left|5V_v+2\right|\right)^{-\frac{5}{2}}

    Grows without bound as 5V + 2 nears 0 · every curve of constant Q·M runs into the point Q = 0, V = −0.4, the volcano's summit, which is why T's pattern converges on the peak

  14. Vv=Pv120cos4(4Qv+3Pv+cos(2Qv+Pv))9500cos2(14Pv8Qv+cos(5Pv3Qv))V_v=P_v-\frac{1}{20}\cos^4\left(4Q_v+3P_v+\cos(2Q_v+P_v)\right)-\frac{9}{500}\cos^2\left(14P_v-8Q_v+\cos(5P_v-3Q_v)\right)
  15. Pv=y+v1000P_v=y+\frac{v}{1000}
  16. Qv=x+v500Q_v=x+\frac{v}{500}

    Channel v = 1 moves the point by (Δx, Δy) = (0.002, 0.001) — this step is where all the shading in the image comes from

Published values
The four terms of H

A pixel's colour is the sum of four terms; switch one off and it becomes zero · sky and mountain are multiplied by (1 − R), so switching the cloud off leaves a cloud-shaped black hole

Shading from a difference

Channel v = 1 nudges the point (x, y) a tiny step, so T₁ − T₀ and C₁ − C₀ are slopes along that direction, which e^(−e^(kx)) turns into lit and shaded sides · turning the direction moves the light · length 0 means no difference, and the whole pattern on the mountain disappears

(Δx, Δy) = (0.0020, 0.0010)
×1.00
×1.00
k = 1000 · 500
Detail from the sums

T, A and B are sums of 50 layers of cosine waves, each finer than the last · with fewer terms the cloud edge and the mountain surface smooth out, and the cloud's shape changes too, because A and B sit inside the mask R · these terms are the main cost of the image — watch the draw time under it

50/50
LAB.08 — A shader runs the same equations as render.py on the GPU of whatever device opens this page · it opens at the published values, every one of them · measured on the author's integrated GPU at 2000×1200: 99.62% of pixels match render.py exactly and the rest differ by at most 1 per colour channel · the original image is not on this page, so there is no live similarity figure

What was learned

What came out of this is the shading mechanism, not a number. The equations have no light source and no surface normal at all; channel v = 1 simply moves the point (x, y) by (0.002, 0.001), so T₁ − T₀ and C₁ − C₀ are finite-difference slopes of a surface along that direction, and e^(−e^(kx)) turns the slope into a lit side and a shaded side. This was confirmed by playing with it directly: set the length to 0 and the whole pattern on the mountain disappears into one flat colour, which means every mark on the mountain is the shading of a surface T the image never draws; turn the direction 180° and lit and shaded sides swap. The step is also not sharp like LAB.06's: the median |k · difference| is about 0.9 on the mountain and 2.9 in the cloud, on the curved part of e^(−e^z) rather than its cliff, so the shading comes out graded instead of black and white. Moved onto the GPU, the colours match numpy exactly on 99.62% of pixels at 2000×1200 and differ by at most 1 per channel on the rest, at about 30–40 ms per image on the author's integrated GPU (about 165 ms for the first image; numpy takes 65 seconds). The per-term constants that depend only on s are computed beforehand in float64 in JavaScript and passed in as a table, so the GPU never has to take the cosine of an angle as large as 7s² at s = 50 (cos(17,500)) in float32 itself.

What fell short

Compared pixel by pixel with the original, using LAB.06's threshold (a summed difference over the three channels of at most 60 out of 765), 94.1% matches and 5.9% does not, with a mean difference of 16 out of 765. The sky matches at 99.7%, but the cloud's texture at 88.9%, the mountain's surface at 88.5% and the lightning at 72.3%. All of the mismatch sits at the scale of 1–2 pixels: shrink both images by 2 and 99.49% matches, by 4 and 99.99% matches, while shifting the render by one pixel in any direction drops the match to 87.8% or less, so the two are aligned. What causes the remaining difference is still not separable: the only copy of the original is a JPEG enlarged by 1.024, and merely sending the render itself down a guessed version of that path (enlarge by 1.024, then save as JPEG) pushes 0.8–2.9% of its own pixels past the threshold, depending on the guessed enlargement; the rest may come from the real enlargement method or the software he computed with, and neither is known. The playable version was measured on one GPU only; phones and other GPU makers have not been measured for accuracy or speed. The constants were not bent to make the picture closer: this entry measures the published equations, it does not fit the image.

Line of experiments

  1. LAB.06 Drawing a sea wave with equations alone
  2. LAB.08 Drawing an active volcano with equations alone (this entry)