You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Stimulus控制器将Active Storage图片加载到Canvas?

如何在Stimulus控制器中加载Active Storage图片到Three.js

问题背景

技术栈为Rails、esbuild/jsbundling-rails、Active Storage、Stimulus JS、Three.js。现有User和Photo模型,Photo模型配置了has_one_attached :image,当前在index页面用Stimulus控制器渲染Three.js场景,但使用的是本地打包图片,需要替换为Active Storage中的单张图片。

解决步骤

1. 将Active Storage图片URL传入Stimulus控制器

在index.html.erb中,修改canvas标签的data属性,把目标图片的Active Storage URL通过Stimulus的value语法传递给控制器。这里以获取第一个Photo实例的图片为例:

<%= content_tag(:canvas, "", data: {
  controller: "threejs",
  threejs_image_url_value: rails_blob_url(Photo.first.image)
}) %>

如果需要指定特定Photo,只需把Photo.first替换为对应的实例即可。

2. 修改Stimulus控制器,使用传入的图片URL

打开你的threejs Stimulus控制器,做以下修改:

  • 移除本地图片的导入语句:import TentImage from "../images/tent.png";
  • 添加Stimulus value定义,用来接收传入的图片URL
  • 在loadTexture方法中使用传入的URL替换原来的本地图片路径

修改后的控制器核心代码如下:

import { Controller } from "@hotwired/stimulus";
import * as THREE from "three";
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import GUI from 'lil-gui';

// Connects to data-controller="threejs"
export default class extends Controller {
  // 定义接收图片URL的value
  static values = {
    imageUrl: String
  }

  connect() {
    this.initThreeJS();
    this.loadTexture();
  }

  loadTexture() {
    const textureLoader = new THREE.TextureLoader();
    // 使用传入的Active Storage图片URL
    textureLoader.load(this.imageUrlValue, (texture) => {
      this.texture = texture;
      this.rectangles.forEach((rectangle) => {
        rectangle.material.map = this.texture;
        rectangle.material.needsUpdate = true;
      });
      console.log("Texture loaded!", texture);
    });
  }

  // 其他Three.js初始化代码保持不变...
  initThreeJS() {
    /** Debug */
    this.gui = new GUI();

    /** Scene, Camera, Renderer */
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
    this.renderer = new THREE.WebGLRenderer();
    this.renderer.setClearColor(0xFFFFFF);
    this.sizes = {
      width: window.innerWidth,
      height: window.innerHeight
    }
    this.renderer.setSize( this.sizes.width, this.sizes.height );
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    document.body.appendChild(this.renderer.domElement);

    /** Circle */
    const circleGeometry = new THREE.CircleGeometry( 2, 32 );
    this.wireframeMaterial = new THREE.MeshBasicMaterial( {
      color: 0x00ff00,
      wireframe: true
    } );
    this.circle = new THREE.Mesh( circleGeometry, this.wireframeMaterial );

    this.circle.rotation.x = Math.PI / - 3;

    /** Add Rectangles around the Circle */
    this.rectangles = this.addRectanglesToCircle(30, 2);
    // 移除重复的loadTexture调用,避免重复加载

    /** Debug Variables */
    this.rectangles.forEach((rectangle) => {
      this.gui.add(rectangle.rotation, 'y').min(- 3).max(3).step(0.01).name('rotationY');
    });

    /** Grid & Axis Helper */
    this.gridHelper = new THREE.GridHelper( 10, 10 );
    this.axesHelper = new THREE.AxesHelper(5);

    this.scene.add(
      this.gridHelper,
      this.circle,
      this.axesHelper);

    this.camera.position.z = 5;

    this.renderer.render( this.scene, this.camera );

    /** Orbit Controls */
    this.controls = new OrbitControls(this.camera, this.renderer.domElement);

    this.animate();
  }

  addRectanglesToCircle(numberOfRectangles, circleRadius) {
    const rectangleHeight = 0.5;
    const rectangleWidth = 1
    const rectangleGeometry = new THREE.PlaneGeometry(rectangleWidth, rectangleHeight);
    const rectangleMaterial = new THREE.MeshBasicMaterial({
      side: THREE.DoubleSide,
      map: this.texture || null
    });
    const rectangles = [];

    for (let i = 0; i < numberOfRectangles; i++) {
      const theta = (i / numberOfRectangles) * 2 * Math.PI;
      const rectangle = new THREE.Mesh(rectangleGeometry, rectangleMaterial);

      rectangle.position.x = circleRadius * Math.sin(theta);
      rectangle.position.y = circleRadius * Math.cos(theta);

      rectangle.rotation.z = 0;
      rectangle.rotation.x = Math.PI / 2;
      rectangle.rotation.y = Math.PI / 2 - theta;

      this.circle.add(rectangle);
      rectangles.push(rectangle);
    }
    return rectangles;
  }

  animate() {
    requestAnimationFrame(this.animate.bind(this));
    this.renderer.render(this.scene, this.camera);
  }
}

注意事项

  • 确保Active Storage配置正确,通过rails_blob_url生成的URL在浏览器中能正常打开图片
  • 若使用外部存储服务出现CORS问题,需在Rails配置中设置对应CORS规则

内容的提问来源于stack exchange,提问作者Inês Lucas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 04:07:33