3

I'd like to get help from Geometry / Wolfram Mathematica people. I want to visualize this 3D Rose in JavaScript (p5.js) environment.

The rose figure I expect to generate

This figure is originally generated using wolfram language by Paul Nylanderin 2004-2006, and below is the code:

Rose[x_, theta_] := Module[{
  phi = (Pi/2)Exp[-theta/(8 Pi)], 
  X = 1 - (1/2)((5/4)(1 - Mod[3.6 theta, 2 Pi]/Pi)^2 - 1/4)^2}, 
  y = 1.95653 x^2 (1.27689 x - 1)^2 Sin[phi]; 
  r = X(x Sin[phi] + y Cos[phi]); 
  {r Sin[theta], r Cos[theta], X(x Cos[phi] - y Sin[phi]), EdgeForm[]
}];

ParametricPlot3D[
  Rose[x, theta], {x, 0, 1}, {theta, -2 Pi, 15 Pi}, 
  PlotPoints -> {25, 576}, LightSources -> {{{0, 0, 1}, RGBColor[1, 0, 0]}}, 
  Compiled -> False
]

I tried implement that code in JavaScript like this below.

function rose(){
  for(let theta = 0; theta < 2700; theta += 3){
    beginShape(POINTS);
    for(let x = 2.3; x < 3.3; x += 0.02){
      let phi = (180/2) * Math.exp(- theta / (8*180));
      let X = 1 - (1/2) * pow(((5/4) * pow((1 - (3.6 * theta % 360)/180), 2) - 1/4), 2);
      let y = 1.95653 * pow(x, 2) * pow((1.27689*x - 1), 2) * sin(phi);
      let r = X * (x*sin(phi) + y*cos(phi));

      let pX = r * sin(theta);
      let pY = r * cos(theta);
      let pZ = (-X * (x * cos(phi) - y * sin(phi)))-200;
  
      vertex(pX, pY, pZ);
    }
    endShape();
  }
}

But I got this result below

Result of my JS implementation Result of my JS implementation

Unlike original one, the petal at the top is too stretched.

I suspected that the

let y = 1.95653 * pow(x, 2) * pow((1.27689*x - 1), 2) * sin(phi);

may should be like below...

let y = pow(1.95653*x, 2*pow(1.27689*x - 1, 2*sin(theta)));

But that went even further away from the original.

Result of my JS implementation

Maybe I'm asking a dumb question, but I've been stuck for several days.

If you see a mistake, please let me know. Thank you in advanse


Update:

I changed the x range to 0~1 as defined by the original one. Also simplified the JS code like below to find the error.

function rose_debug(){
  for(let theta = 0; theta < 15*PI; theta += PI/60){
    beginShape(POINTS);
    for(let x = 0.0; x < 1.0; x += 0.005){
      let phi = (PI/2) * Math.exp(- theta / (8*PI));
      let y = pow(x, 4) * sin(phi);
      let r = (x * sin(phi) + y * cos(phi));

      let pX = r * sin(theta);
      let pY = r * cos(theta);
      let pZ = x * cos(phi) - y * sin(phi);
      vertex(pX, pY, pZ);
    }
    endShape();
  }
}

But the result still keeps the wrong proportion↓↓↓ enter image description here

Also, when I remove the term "sin(phi)" in the line "let y =..." like below

let y = pow(x, 4);

then I got a figure somewhat resemble the original like below enter image description here

At this moment I was starting to suspect the mistake on the original equation, but I found another article by Jorge García Tíscar(Spanish) that implemented the exact same 3D rose in wolfram language successfully.

enter image description here

So, now I really don't know how the original is formed by the equation


Update2: Solved

I followed a suggestion by Trentium (Answer No.2 below) that stick to 0 ~ 1 as the range of x, then multiply the r and X by an arbitrary number.

for(let x = 0; x < 1; x += 0.05){
r = r * 200;
X = X * 200;

Then I got this correct result looks exactly the same as the original enter image description here

Simplified final code:

function rose_debug3(){
  for(let x = 0; x <= 1; x += 0.05){
    beginShape(POINTS);
    for(let theta = -2*PI; theta <= 15*PI; theta += 17*PI/2000){
      let phi = (PI / 2) * Math.exp(- theta / (8 * PI));
      let X = 1 - (1/2) * ((5/4) * (1 - ((3.6 * theta) % (2*PI))/PI) ** 2 - 1/4) ** 2;
      let y = 1.95653 * (x ** 2) * ((1.27689*x - 1) ** 2) * sin(phi);
      let r = X * (x * sin(phi) + y * cos(phi));

      if(0 < r){
        const factor = 200;
        let pX = r * sin(theta)*factor;
        let pY = r * cos(theta)*factor;
        let pZ = X * (x * cos(phi) - y * sin(phi))*factor;
        vertex(pX, pY, pZ);
      }
    }
    endShape();
  }
}

The reason I got the vertically stretched figure at first was the range of the x. I thought that changing the range of the x just affect the whole size of the figure. But actually, the range affects like this below.

(1): 0 ~ x ~ 1, (2): 0 ~ x ~ 1.2

enter image description hereenter image description here

(3): 0 ~ x ~ 1.5, (4): 0 ~ x ~ 2.0

enter image description hereenter image description here

(5): flipped the (4)

enter image description here

So far I saw the result like (5) above, didn't realize that the correct shape was hiding inside that figure.

Thank you Trentium so much for kindly helping me a lot!

  • The Wolfram language reference indicates `Rose[x, theta], {x, 0, 1}, {theta, -2 Pi, 15 Pi}`, which I'm interpreting as `x` ranging from `0 to 1`, and `theta` ranging from `-2pi to 15pi`, but I don't see where that's implemented in the javascript code. Ie, seems like you have to incorporate a nested pair of `for` loops that provides all combinations of `x` and `theta` to generate the points... – Trentium Feb 19 '22 at 03:50
  • Oh, I've implemented the nested for loop like my JS code above I just changed the range of the x to 2.3 ~ 3.3 to make the whole size larger a bit. – Kazuki Umeda Feb 19 '22 at 04:11
  • Ah, and I just noticed the `theta` loop. Can you post your latest updated code? Also, it might be worthwhile to use the ranges of `x` and `theta` as defined by the Wolfram code, just to ensure that you're getting the same results. (Also, your `y` might appear to be off due to the aspect ratio between `x` and `y`...) – Trentium Feb 19 '22 at 04:16
  • I just put an update on my question above:)))) I just tried to remap to 0 ~ 1, it just shrank but keeps the same wrong proportion. – Kazuki Umeda Feb 19 '22 at 06:02
  • I just sent an e-mail to Paul Nylander who created the original one. A tiny hope, but very happy if he replies. – Kazuki Umeda Feb 19 '22 at 07:22
  • 1
    Posted another answer, with a rendering in ThreeJS showing the end result which now takes the shape of the rose as in the Wolfram references. The key is to only add the point if `0 < r`, as otherwise there are long extraneous points emanating from the rose. Without this check, these extraneous arms overwhelm the rose. I suspect that if you zoomed in on your P5 model towards the center of the points that you'd see a tiny properly shaped rose... See my second answer... – Trentium Feb 19 '22 at 17:12
  • 2
    ↑ I just noticed this comment, Yes! that was exactly as you told me:))) – Kazuki Umeda Feb 20 '22 at 06:29
  • 1
    Here is a bouquet: https://laustep.github.io/stlahblog/frames/threejs_RosesBouquet.html – Stéphane Laurent Feb 21 '22 at 21:28
  • Wow a beautiful spherical bouquet, thank you for that – Kazuki Umeda Feb 23 '22 at 14:48

2 Answers2

3

Since this response is a significant departure from my earlier response, am adding a new answer...

In rendering the rose algorithm in ThreeJS (sorry, I'm not a P5 guy) it became apparent that when generating the points, that only the points with a positive radius are to be rendered. Otherwise, superfluous points are rendered far outside the rose petals.

(Note: When running the code snippet, use the mouse to zoom and rotate the rendering of the rose.)

<script type="module">

  import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.115.0/build/three.module.js';
  import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.115.0/examples/jsm/controls/OrbitControls.js';

  //
  // Set up the ThreeJS environment.
  //
  var renderer = new THREE.WebGLRenderer();
  renderer.setSize( window.innerWidth, window.innerHeight );
  document.body.appendChild( renderer.domElement );

  var camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 );
  camera.position.set( 0, 0, 100 );
  camera.lookAt( 0, 0, 0 );

  var scene = new THREE.Scene();
  
  let controls = new OrbitControls(camera, renderer.domElement);

  //
  // Create the points.
  //
  function rose( xLo, xHi, xCount, thetaLo, thetaHi, thetaCount ){
    let vertex = [];
    let colors = [];
    let radius = [];
    for( let x = xLo; x <= xHi; x += ( xHi - xLo ) / xCount ) {
      for( let theta = thetaLo; theta <= thetaHi; theta += ( thetaHi - thetaLo ) / thetaCount ) {
        let phi = ( Math.PI / 2 ) * Math.exp( -theta / ( 8 * Math.PI ) );
        let X = 1 - ( 1 / 2 ) * ( ( 5 / 4 ) * ( 1 - ( ( 3.6 * theta ) % ( 2 * Math.PI ) ) / Math.PI ) ** 2 - 1 / 4 ) ** 2;
        let y = 1.95653 * ( x ** 2 ) * ( (1.27689 * x - 1) ** 2 ) * Math.sin( phi );
        let r = X * ( x * Math.sin( phi ) + y * Math.cos( phi ) ); 

        //
        // Fix: Ensure radius is positive, and scale up accordingly...
        //
        if ( 0 < r ) {
        
          const factor = 20;
          
          r = r * factor;
          radius.push( r );
          X = X * factor;

          vertex.push( r * Math.sin( theta ), r * Math.cos( theta ), X * ( x * Math.cos( phi ) - y * Math.sin( phi ) ) );
        }
      }
    }
    
    //
    // For the fun of it, lets adjust the color of the points based on the radius
    // of the point such that the larger the radius, the deeper the red.
    //
    let rLo = Math.min( ...radius );
    let rHi = Math.max( ...radius );
    for ( let i = 0; i < radius.length; i++ ) {
      let clr = new THREE.Color( Math.floor( 0x22 + ( 0xff - 0x22 ) * ( ( radius[ i ] - rLo ) / ( rHi - rLo ) ) ) * 0x10000 + 0x002222 );
      colors.push( clr.r, clr.g, clr.b );
    }
    
    return [ vertex, colors, radius ];
  }

  //
  // Create the geometry and mesh, and add to the THREE scene.
  //
  const geometry = new THREE.BufferGeometry();
  
  
  let [ positions, colors, radius ] = rose( 0, 1, 20, -2 * Math.PI, 15 * Math.PI, 2000 );
  
  geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
  geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );

  const material = new THREE.PointsMaterial( { size: 4, vertexColors: true, depthTest: false, sizeAttenuation: false } );

  const mesh = new THREE.Points( geometry, material );
  scene.add( mesh );
        
  //
  // Render...
  // 
  var animate = function () {
    requestAnimationFrame( animate );
    renderer.render( scene, camera );
  };

  animate();
</script>

Couple of notables:

  • When calling rose( xLo, xHi, xCount, thetaLo, thetaHi, thetaCount ), the upper range thetaHi can vary from Math.PI to 15 * Math.PI, which varies the number of petals.
  • Both xCount and thetaCount vary the density of the points. The Wolfram example uses 25 and 576, respectively, but this is to create a geometry mesh, whereas if creating a point field the density of points needs to be increased. Hence, in the code the values are 20 and 2000.

Enjoy!

Trentium
  • 3,419
  • 2
  • 12
  • 19
  • I followed your way and finally got the result exactly the same as the original!!! So far I thought that changing the range of the x just affects the whole size of the figure. But actually, the more the range goes up positive way, the petal shape goes hang down. I finally realize that (I add the update above again) I can't thank you enough for this – Kazuki Umeda Feb 20 '22 at 06:20
  • 1
    Plus, I actually run a small coding YouTube channel. https://www.youtube.com/channel/UCACzb9JwH0ppt9Xwcpz9Bmw/featured I intend to make a tutorial with this 3D Rose. So would you mind if I mention about you for helping me in my video? – Kazuki Umeda Feb 20 '22 at 06:24
  • @KazukiUmeda some people do Sudoku or other puzzles for fun. I do stackoverflow. Glad I could assist. Rather than mentioning me specifically, it might be better to mention that stackoverflow.com is a helpful resource, because there's a lot of folks on this site that are smarter than me... I perused your face mesh videos, which is cool stuff. You should take it one step further, and map a [toonified](https://toonify.photos/) face to a face mesh, and then map that in turn to your program. That way, someone can see themselves toonified in live action! – Trentium Feb 20 '22 at 12:34
  • Oh, thank you for watching the videos. I visited the Toonified, that's very exciting stuff. I'll play around with that near future. Maybe I'll be able to make the tutorial series like you suggested – Kazuki Umeda Feb 23 '22 at 14:46
  • @Trentium I was wondering if you could describe how to convert the mesh to a solid geometry? I'm trying to replicate these flowers in THREE.JS and I found out how to use a THREE.ParametricGeometry and that works fine but I realized that your method is a lot less computationally intense and higher fidelity than mine, but I can't seem to figure out how to convert your method to a solid geometry that doesn't use a points material, would you be willing to share any tips on this? – R-yurew Dec 30 '22 at 21:44
  • @R-yurew take a look at the first 5 minutes of KazukiUmeda's [video](https://www.youtube.com/watch?v=FrnzGzJZvAY) where he tweaks the algorithm to create a 2 dimensional array as the basis for the mesh. Of course, he's using P5, but in your case, you can then convert this 2 dimensional array into a three.js mesh via [BufferGeometry](https://threejs.org/docs/#api/en/core/BufferGeometry)... – Trentium Dec 31 '22 at 00:04
  • @Trentium Sorry to keep bothering you but I decided to try your suggestion, and I used the 2D array based on Kazuki's video and I got [this result!](https://codepen.io/ricky1280/pen/OJwXWZB) do you know why it looks like this, and not like the rose in his video? It's the exact same code and equations but the way the mesh behaves is completely different and the points actually go waaay below where they're supposed to? would you be open to helping? – R-yurew Jan 02 '23 at 00:25
  • I took a quick look at your code, and one of the main issues is that Math.sin and Math.cos operate in radians, whereas the values being passed (in the original P5 code) are in degrees... – Trentium Jan 02 '23 at 15:07
  • @Trentium thank you for catching that! [The points are in the right place!](https://codepen.io/ricky1280/pen/LYBZrjw) but the faces still aren't defined properly :/ I've asked on other forums as well but I feel like this will be my eternal struggle, I've watched and rewatched Kazuki's videos a lot of times but I still can't figure it out, but that's not really your problem (unless you want it to be? ) Thank you for your help with this! – R-yurew Jan 02 '23 at 20:06
  • @R-yurew I believe the issue now with your code is that in P5, the positions array is built via 4 points (ie, essentially defining the surface via rectangles) whereas in ThreeJS the geometry is built via triangles. Thus, for each P5 rectangle, the ThreeJS code will need to define 2 triangles, or 6 points per iteration within the Theta loop... – Trentium Jan 02 '23 at 22:52
  • @Trentium Alright I've finally had time to look at this again, [the indices have been changed based on the order I've added the positions to the array and it looks better](https://codepen.io/ricky1280/pen/bGjBJQP?editors=1010) I used [this so page](https://stackoverflow.com/questions/16498296/three-js-face4-generates-triangle-instead-of-square) but for some reason the actual form of the geometry doesn't match the equation even though the equation is the same? – R-yurew Jan 07 '23 at 02:17
  • @Trentium... [never mind ;)](https://codepen.io/ricky1280/pen/mdjOYba?editors=1010) – R-yurew Jan 07 '23 at 02:37
  • @R-yurew looks great! Suggest posting your results as an answer to your [related question](https://stackoverflow.com/questions/74966462), both of which I'll upvote. – Trentium Jan 07 '23 at 11:01
  • @Trentium thank you for your help and patience! I posted it as an answer. This helped me a great deal – R-yurew Jan 15 '23 at 21:43
1

Presumably the algorithm above is referencing cos() and sin() functions that handle the angles in degrees rather than radians, but wherever using angles while employing non-trigonometric transformations, the result will be incorrect.

For example, the following formula using radians...

  • phi = (Pi/2)Exp[-theta/(8 Pi)]

...has been incorrectly translated to...

  • phi = ( 180 / 2 ) * Math.exp( -theta / ( 8 * 180 ) )

To test, let's assume theta = 2. Using the original formula in radians...

  • phi = ( Math.PI / 2 ) * Math.exp( -2 / ( 8 * Math.PI ) )
  • = 1.451 rad
  • = 83.12 deg

...and now the incorrect version using degrees, which returns a different angle...

  • phi = ( 180 / 2 ) * Math.exp( -2 / ( 8 * 180 ) )
  • = 89.88 deg
  • = 1.569 rad

A similar issue will occur with the incorrectly translated expression...

  • pow( ( 1 - ( 3.6 * theta % 360 ) / 180 ), 2 )

Bottom line: Stick to radians.

P.S. Note that there might be other issues, but using radians rather than degrees needs to be corrected foremost...

Trentium
  • 3,419
  • 2
  • 12
  • 19
  • Thank you for pointing it out! I just change it to radian, but unfortunately, the result didn't change much. I also realized that the y influence the vertical shape strongly, so now I assume I need to fix the y in someway.......... – Kazuki Umeda Feb 19 '22 at 03:32