使用SVGRenderer在Three.js中将图像呈现为节点(或以其他方式呈现球体)

12

我在SVG文档中有一个<circle>元素,我为其应用了一个<radialGradient>来营造球体的假象:

<svg version="1.1" id="sphere_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="640px" height="640px" viewBox="0 0 640 640" enable-background="new 0 0 640 640" xml:space="preserve">
    <defs>
        <radialGradient id="sphere_gradient" cx="292.3262" cy="287.4077" r="249.2454" fx="147.7949" fy="274.5532" gradientTransform="matrix(1.0729 0 0 1.0729 -23.3359 -23.3359)" gradientUnits="userSpaceOnUse">
            <stop id="sphere_gradient_0" offset="0" style="stop-color:#F37D7F"/>
            <stop id="sphere_gradient_1" offset="0.4847" style="stop-color:#ED1F24"/>
            <stop id="sphere_gradient_2" offset="1" style="stop-color:#7E1416"/>
        </radialGradient>
    </defs>
    <circle fill="url(#sphere_gradient)" cx="320" cy="320" r="320"/>
</svg>

它看起来像这样:

红色球体

JSFiddle

我可以使用Gabe Lerner的canvg在三.js WebGLRenderer容器中呈现它:

/* sphere_asset is a div containing the svg element */
var red_svg_html = new String($('#sphere_asset').html()); 
var red_svg_canvas = document.createElement("canvas");
canvg(red_svg_canvas, red_svg_html);
var red_svg_texture = new THREE.Texture(red_svg_canvas);
var red_particles = new THREE.Geometry();
var red_particle_material = new THREE.PointCloudMaterial({ 
    map: red_svg_texture, 
    transparent: true, 
    size: 0.15, 
    alphaTest: 0.10 
});
var red_particle_count = 25;
for (var p = 0; p < red_particle_count; p++) {
    var pX = 0.9 * (Math.random() - 0.5),
        pY = 0.9 * (Math.random() - 0.5),
        pZ = 0.9 * (Math.random() - 0.5),
        red_particle = new THREE.Vector3(pX, pY, pZ);
    red_particles.vertices.push(red_particle);
}
var red_particle_system = new THREE.PointCloud(red_particles, red_particle_material);
scene.add(red_particle_system);

目前为止,一切都很好。我甚至可以以编程方式修改渐变并呈现不同类别的粒子:

spheres

现在我想要做的是从WebGLRenderer切换到使用SVGRenderer,这样我就可以允许最终用户设置所需的方向,然后导出矢量图像(SVG或在后端转换为PDF),可用于出版质量的工作。

使用three.js的SVG sandbox示例作为实验基础,我尝试了几种不同的技术,但没有取得太大的成功。我希望有经验的three.js用户能提供一些建议。

我的第一次尝试是使用canvg将SVG渲染成PNG图像,然后应用于一个<image>节点:

var red_svg_html = new String($('#sphere_asset').html());
var red_svg_canvas = document.createElement("canvas");
canvg(red_svg_canvas, red_svg_html);
var red_png_data = red_svg_canvas.toDataURL('image/png');
var red_node = document.createElementNS('http://www.w3.org/2000/svg', 'image');
red_node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', red_png_data);
red_node.setAttributeNS('http://www.w3.org/2000/svg', 'height', '10');
red_node.setAttributeNS('http://www.w3.org/2000/svg', 'width', '10');
var red_particle_count = 25;
for (var i = 0; i < red_particle_count; i++) {
    var object = new THREE.SVGObject(red_node.cloneNode());
    object.position.x = 0.9 * (Math.random() - 0.5);
    object.position.y = 0.9 * (Math.random() - 0.5);
    object.position.z = 0.9 * (Math.random() - 0.5);
    scene.add(object);
}

我的视窗中没有显示任何节点。

接下来我尝试使用canvgTHREE.Texture例程创建 THREE.Sprite对象:

var red_svg_html = new String($('#sphere_asset').html());
var red_svg_canvas = document.createElement("canvas");
canvg(red_svg_canvas, red_svg_html);
var red_svg_texture = new THREE.Texture(red_svg_canvas);
red_svg_texture.needsUpdate = true;
var red_sprite = THREE.ImageUtils.loadTexture(red_png_data);
var red_particle_count = 25;
for (var p = 0; p < red_particle_count; p++) {
    var material = new THREE.SpriteMaterial( { 
        map: red_svg_texture, 
        transparent: true, 
        size: 0.15, 
        alphaTest: 0.10 
    });
    var sprite = new THREE.Sprite( material );
    sprite.position.x = 0.9 * (Math.random() - 0.5),
    sprite.position.y = 0.9 * (Math.random() - 0.5),
    sprite.position.z = 0.9 * (Math.random() - 0.5),
    sprite.scale.set(0.1, 0.1, 0.1);
    scene.add(sprite);
}

这次稍微好一些,因为在渲染视图框中,球体所在的位置出现了白色不透明的方框。

第三次尝试创建一个<svg>嵌套在父级SVG节点中,其中包含可引用的id为#sphere_gradient<radialGradient>

var xmlns = "http://www.w3.org/2000/svg";
var svg = document.createElementNS(xmlns, 'svg');
svg.setAttributeNS(null, 'version', '1.1');
svg.setAttributeNS(null, 'x', '0px');
svg.setAttributeNS(null, 'y', '0px');
svg.setAttributeNS(null, 'width', '640px');
svg.setAttributeNS(null, 'height', '640px');
svg.setAttributeNS(null, 'viewBox', '0 0 640 640');
svg.setAttributeNS(null, 'enable-background', 'new 0 0 640 640');

var defs = document.createElementNS(xmlns, "defs");
var radialGradient = document.createElementNS(xmlns, "radialGradient");
radialGradient.setAttributeNS(null, "id", "sphere_gradient");
radialGradient.setAttributeNS(null, "cx", "292.3262");
radialGradient.setAttributeNS(null, "cy", "287.4077");
radialGradient.setAttributeNS(null, "r", "249.2454");
radialGradient.setAttributeNS(null, "fx", "147.7949");
radialGradient.setAttributeNS(null, "fy", "274.5532");
radialGradient.setAttributeNS(null, "gradientTransform", "matrix(1.0729 0 0 1.0729 -23.3359 -23.3359)");
radialGradient.setAttributeNS(null, "gradientUnits", "userSpaceOnUse");

var stop0 = document.createElementNS(null, "stop");
stop0.setAttributeNS(null, "offset", "0");
stop0.setAttributeNS(null, "stop-color", "#f37d7f");
radialGradient.appendChild(stop0);

var stop1 = document.createElementNS(null, "stop");
stop1.setAttributeNS(null, "offset", "0.4847");
stop1.setAttributeNS(null, "stop-color", "#ed1f24");
radialGradient.appendChild(stop1);

var stop2 = document.createElementNS(null, "stop");
stop2.setAttributeNS(null, "offset", "1");
stop2.setAttributeNS(null, "stop-color", "#7e1416");
radialGradient.appendChild(stop2);

defs.appendChild(radialGradient);

svg.appendChild(defs);

var red_circle = document.createElementNS(xmlns, "circle")
red_circle.setAttribute('fill', 'url(#sphere_gradient)');
red_circle.setAttribute('r', '320');
red_circle.setAttribute('cx', '320');
red_circle.setAttribute('cy', '320');
svg.appendChild(red_circle);

var red_particle_count = 25;
for (var i = 0; i < red_particle_count; i++) {
    var object = new THREE.SVGObject(svg.cloneNode(true));
    object.position.x = 0.85 * (Math.random() - 0.5);
    object.position.y = 0.85 * (Math.random() - 0.5);
    object.position.z = 0.85 * (Math.random() - 0.5);
    scene.add(object);
}

没有任何节点被渲染出来。调整<circle>元素的rcxcy属性并不会改变最终结果。

有趣的是,如果我将fill属性从url(#sphere_gradient)更改为red,我会得到一个大圆,大部分渲染在我的视口之外,它与场景没有连接(它不会像立方体的侧面那样随着父场景中的其他元素旋转)。

是否有一种(工作且性能良好的)方法可以使用three.js中的SVGRenderer绘制空间中的球形或圆角的类球状粒子?

3个回答

2

SVG中的大多数属性都在null命名空间中,因此

red_node.setAttributeNS('http://www.w3.org/2000/svg', 'height', '10');
red_node.setAttributeNS('http://www.w3.org/2000/svg', 'width', '10');

正确的写法是

red_node.setAttribute('height', '10');
red_node.setAttribute('width', '10');

FWIW

red_node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', red_png_data);

最好写成这样:

red_node.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', red_png_data);

虽然在大多数用户代理中,您的原始表单将起作用。

在最后一个示例中,停止元素必须位于SVG命名空间中,即:

var stop0 = document.createElementNS(null, "stop");

应该是

var stop0 = document.createElementNS(xmlns, "stop");

没有渐变停止点的渐变根本不会被绘制,所以在将填充颜色更改为红色之前您看不到任何东西。

SVG采用绘图模型。物体按照它们在文件中出现的顺序进行绘制。如果您想要某些东西放在其他东西上面,您需要在文件中稍后放置它。


感谢您的建议。将href属性修改为xlink:href并没有改变最终结果(即,仍然没有渲染任何节点)。 - Alex Reynolds
好的,谢谢 - 命名空间的更改有助于应用渐变! 然而,看起来应用于这个嵌套的<svg>元素的转换没有被计算到场景中,所以其中包含的<circle>元素保持固定在其位置,即使对其父级<svg>应用了转换。 - Alex Reynolds
我已经成功地将<defs>组移动到SVGRenderer代码中,使其成为根级别的<svg>的一部分,但是我遇到了渲染问题,其中<circle>元素没有按顺序绘制,这破坏了深度感的错觉。具体来说,靠近相机的<circle>元素首先被绘制,然后才是离相机更远的另一个元素。结果是,靠近相机的元素(应该在前景中)被渲染在更远的元素下面。旋转场景会破坏深度感的错觉。 - Alex Reynolds

0
在你的第一次尝试中,如果你添加了snap svg库,你可以从canvg获取svg元素并将其包装成snap结构(作为snap的画布),然后使用paper.toString()方法生成你的svg文件的svg代码。
var paper = Snap("#... ");        //this wraps the svg element from the canvg onto snap structure 
console.log( paper.toString());   // this gives you the svg code!

编辑:或者您可以使用fabric.js将画布转换为SVG:

var svg = canvas.toSVG();

或者您可以使用canvas2svg库。


我已经有了SVG文件。我需要将其从SVG转换为PNG,再转换回SVG吗? - Alex Reynolds
你已经有了初始的SVG,而不是更新后的SVG。以下是我认为可以帮助你的步骤:你有一个SVG,然后将其添加到画布中以添加额外内容,然后从画布中获取更新后的SVG。你最后一部分无法获取更新后的SVG。这难道不是你想要简化流程的方式吗?! - Bayan
我不清楚你所说的“更新”是什么意思。SVG 是自包含的;我已经将它作为字符串引入了:var red_svg_html = new String($('#sphere_asset').html()); - Alex Reynolds
我所指的更新的svg是你希望用户导出的svg图像。"我想要做的是从WebGLRenderer切换到使用SVGRenderer,这样我就可以允许最终用户设置所需的方向,然后导出矢量图像..." 使用SVGRenderer无法实现你想要的功能,它有限制。因此,你需要使用webGLrenderer,然后使用我提到的库之一,将画布的所需方向导出为svg。 - Bayan
你能在 jsbin 上放上你的第三个方法吗?我认为我有你需要的答案! - Bayan

0

以下是需要添加到你的SVGRenerer.js中,以解决粒子渲染问题的内容:

function renderParticle( v1, element, material, scene ) {


        _svgNode = getCircleNode( _circleCount++ );
        _svgNode.setAttribute( 'cx', v1.x );
        _svgNode.setAttribute( 'cy', v1.y );
        _svgNode.setAttribute( 'r', element.scale.x * _svgWidthHalf );

        if ( material instanceof THREE.ParticleCircleMaterial ) {

            if ( _enableLighting ) {

                _color.r = _ambientLight.r + _directionalLights.r + _pointLights.r;
                _color.g = _ambientLight.g + _directionalLights.g + _pointLights.g;
                _color.b = _ambientLight.b + _directionalLights.b + _pointLights.b;

                _color.r = material.color.r * _color.r;
                _color.g = material.color.g * _color.g;
                _color.b = material.color.b * _color.b;

                _color.updateStyleString();

            } else {

                _color = material.color;

            }

            _svgNode.setAttribute( 'style', 'fill: ' + _color.__styleString );

        }

        _svg.appendChild( _svgNode );


    }

谢谢,但正如之前的评论所提到的,我在自定义SVGRenderer时遇到了两个问题:1.我无法轻松地分配程序生成的径向渐变,因此我被困在我创建的任何渐变中。2.最终结果没有呈现出正确的3D视图,即靠近相机的元素不会呈现在远离相机的元素之上,并且缩放不正确。旋转粒子可以展示这种“破坏”。目前还不清楚如何重新排序<circle>元素以呈现正确的3D场景。这两个问题都是不能接受的。 - Alex Reynolds
你能提供一下你现在使用的SVGRender.js文件的链接吗? - Bayan
好的,请使用这个文件,如果可以的话请告诉我:https://dl.dropboxusercontent.com/u/140225334/SVGRenderer.js - Bayan
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Alex Reynolds
好的,你能否在jsfiddle或jsbin上分享你的设置,或者将它通过邮件发送给我以便我为你检查。 - Bayan
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接