将 Wolfram 语言编写的 3D Rose 移植到 JavaScript
Porting 3D Rose written by Wolfram Language into JavaScript
我想得到 Geometry / Wolfram Mathematica 人员的帮助。
我想在 JavaScript (p5.js) 环境中可视化此 3D 玫瑰。
此图最初由Paul Nylander于2004-2006年使用wolfram语言生成,下面是代码:
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
]
我尝试像下面这样在 JavaScript 中实现该代码。
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();
}
}
但是我得到下面这个结果
与原来的不同,顶部的花瓣被拉得太长了。
我怀疑
let y = 1.95653 * pow(x, 2) * pow((1.27689*x - 1), 2) * sin(phi);
可能应该像下面这样...
let y = pow(1.95653*x, 2*pow(1.27689*x - 1, 2*sin(theta)));
但这与原来的相去甚远。
也许我问了一个愚蠢的问题,但我已经卡了好几天了。
如果您看到错误,请告诉我。
谢谢你的支持
更新:
我把x的范围改成了原来定义的0~1。
还简化了如下的JS代码来查找错误。
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();
}
}
但是结果还是保持错误的比例↓↓↓
此外,当我删除“let y =...”行中的术语“sin(phi)”时,如下所示
let y = pow(x, 4);
然后我得到了一个有点像下面的原始图
此刻我开始怀疑原来的方程式有误,但我发现 another article by Jorge García Tíscar(西班牙语)用 wolfram 语言成功地实现了完全相同的 3D 玫瑰。
所以,现在真不知道原来的方程是怎么形成的
更新 2:已解决
我听从了Trentium的建议(下面的答案2)坚持0~1作为x的范围,然后将r和X乘以任意数。
for(let x = 0; x < 1; x += 0.05){
r = r * 200;
X = X * 200;
然后我得到这个正确的结果看起来和原来的一模一样
简化的最终代码:
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();
}
}
一开始我得到垂直拉伸图的原因是x的范围。我认为改变 x 的范围只会影响图形的整体大小。但实际上,范围影响如下。
(1): 0 ~ x ~ 1, (2): 0 ~ x ~ 1.2
(3): 0 ~ x ~ 1.5, (4): 0 ~ x ~ 2.0
(5): 翻转 (4)
到目前为止我看到了上面(5)的结果,没有意识到正确的形状隐藏在那个图形里面。
非常感谢 Trentium 对我的帮助!
大概上面的算法引用了 cos()
和 sin()
函数,它们以度数而不是弧度来处理角度,但是在使用 non-trigonometric 转换时使用角度的任何地方,结果将是不正确。
例如,以下公式使用弧度...
- phi = (Pi/2)Exp[-theta/(8 Pi)]
...已被错误地翻译成...
- phi = ( 180 / 2 ) * Math.exp( -theta / ( 8 * 180 ) )
为了测试,我们假设 theta = 2。使用以弧度为单位的原始公式...
- phi = ( Math.PI / 2 ) * Math.exp( -2 / ( 8 * Math.PI ) )
- = 1.451 弧度
- = 83.12 度
...现在使用度数的错误版本,returns 不同的角度...
- phi = ( 180 / 2 ) * Math.exp( -2 / ( 8 * 180 ) )
- = 89.88 度
- = 1.569 弧度
错误翻译的表达式也会出现类似的问题...
- pow( ( 1 - ( 3.6 * theta % 360 ) / 180 ), 2 )
底线:坚持弧度。
P.S。请注意,可能还有其他问题,但首先需要更正使用弧度而不是度数...
由于此回复与我之前的回复有很大不同,我添加了一个新答案...
在 ThreeJS 中渲染 rose
算法(抱歉,我不是 P5 的人)很明显,在生成点时,只渲染具有正半径的点。否则,多余的点会渲染到远离玫瑰花瓣的地方。
(注:运行代码段时,用鼠标缩放旋转玫瑰花的渲染。)
<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>
两位名人:
- 当调用
rose( xLo, xHi, xCount, thetaLo, thetaHi, thetaCount )
时,上限thetaHi
可以从Math.PI
到15 * Math.PI
变化,这会改变花瓣的数量。
xCount
和thetaCount
都改变了点的密度。 Wolfram 示例分别使用 25 和 576,但这是为了创建几何网格,而如果创建点场,则需要增加点的密度。因此,在代码中,值为 20 和 2000。
尽情享受吧!
我想得到 Geometry / Wolfram Mathematica 人员的帮助。 我想在 JavaScript (p5.js) 环境中可视化此 3D 玫瑰。
此图最初由Paul Nylander于2004-2006年使用wolfram语言生成,下面是代码:
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
]
我尝试像下面这样在 JavaScript 中实现该代码。
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();
}
}
但是我得到下面这个结果
与原来的不同,顶部的花瓣被拉得太长了。
我怀疑
let y = 1.95653 * pow(x, 2) * pow((1.27689*x - 1), 2) * sin(phi);
可能应该像下面这样...
let y = pow(1.95653*x, 2*pow(1.27689*x - 1, 2*sin(theta)));
但这与原来的相去甚远。
也许我问了一个愚蠢的问题,但我已经卡了好几天了。
如果您看到错误,请告诉我。 谢谢你的支持
更新:
我把x的范围改成了原来定义的0~1。 还简化了如下的JS代码来查找错误。
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();
}
}
但是结果还是保持错误的比例↓↓↓
此外,当我删除“let y =...”行中的术语“sin(phi)”时,如下所示
let y = pow(x, 4);
然后我得到了一个有点像下面的原始图
此刻我开始怀疑原来的方程式有误,但我发现 another article by Jorge García Tíscar(西班牙语)用 wolfram 语言成功地实现了完全相同的 3D 玫瑰。
所以,现在真不知道原来的方程是怎么形成的
更新 2:已解决
我听从了Trentium的建议(下面的答案2)坚持0~1作为x的范围,然后将r和X乘以任意数。
for(let x = 0; x < 1; x += 0.05){
r = r * 200;
X = X * 200;
然后我得到这个正确的结果看起来和原来的一模一样
简化的最终代码:
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();
}
}
一开始我得到垂直拉伸图的原因是x的范围。我认为改变 x 的范围只会影响图形的整体大小。但实际上,范围影响如下。
(1): 0 ~ x ~ 1, (2): 0 ~ x ~ 1.2
(3): 0 ~ x ~ 1.5, (4): 0 ~ x ~ 2.0
(5): 翻转 (4)
到目前为止我看到了上面(5)的结果,没有意识到正确的形状隐藏在那个图形里面。
非常感谢 Trentium 对我的帮助!
大概上面的算法引用了 cos()
和 sin()
函数,它们以度数而不是弧度来处理角度,但是在使用 non-trigonometric 转换时使用角度的任何地方,结果将是不正确。
例如,以下公式使用弧度...
- phi = (Pi/2)Exp[-theta/(8 Pi)]
...已被错误地翻译成...
- phi = ( 180 / 2 ) * Math.exp( -theta / ( 8 * 180 ) )
为了测试,我们假设 theta = 2。使用以弧度为单位的原始公式...
- phi = ( Math.PI / 2 ) * Math.exp( -2 / ( 8 * Math.PI ) )
- = 1.451 弧度
- = 83.12 度
...现在使用度数的错误版本,returns 不同的角度...
- phi = ( 180 / 2 ) * Math.exp( -2 / ( 8 * 180 ) )
- = 89.88 度
- = 1.569 弧度
错误翻译的表达式也会出现类似的问题...
- pow( ( 1 - ( 3.6 * theta % 360 ) / 180 ), 2 )
底线:坚持弧度。
P.S。请注意,可能还有其他问题,但首先需要更正使用弧度而不是度数...
由于此回复与我之前的回复有很大不同,我添加了一个新答案...
在 ThreeJS 中渲染 rose
算法(抱歉,我不是 P5 的人)很明显,在生成点时,只渲染具有正半径的点。否则,多余的点会渲染到远离玫瑰花瓣的地方。
(注:运行代码段时,用鼠标缩放旋转玫瑰花的渲染。)
<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>
两位名人:
- 当调用
rose( xLo, xHi, xCount, thetaLo, thetaHi, thetaCount )
时,上限thetaHi
可以从Math.PI
到15 * Math.PI
变化,这会改变花瓣的数量。 xCount
和thetaCount
都改变了点的密度。 Wolfram 示例分别使用 25 和 576,但这是为了创建几何网格,而如果创建点场,则需要增加点的密度。因此,在代码中,值为 20 和 2000。
尽情享受吧!