3JS Viewer in Streamlit

Hi All,

This is my second post about this but I’m trying to embed a 3js viewer in my streamlit app and am having trouble. The viewer just doesn’t render. What is the solution to this?

Thanks

import streamlit as st
import streamlit.components.v1 as components

html_string = '''
<h1>HTML</h1>

<script language="javascript">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { Rhino3dmLoader } from 'three/addons/loaders/3DMLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

let camera, scene, renderer;
let controls, gui;
init();
animate();

function init() {

  THREE.Object3D.DEFAULT_UP.set( 0, 0, 1 );

  renderer = new THREE.WebGLRenderer( { antialias: true } );
  renderer.setPixelRatio( 2 );
  renderer.setSize( window.innerWidth, window.innerHeight );
  renderer.outputEncoding = THREE.sRGBEncoding;
  document.body.appendChild( renderer.domElement );

  camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 10000 );
  camera.position.set( 40, -40, 50 );

  scene = new THREE.Scene();

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
  scene.add(ambientLight);
  
  const directionalLight = new THREE.DirectionalLight( 0xffffff, 2 );
  directionalLight.position.set( 20, 40, 100);
  scene.add( directionalLight );

  const loader = new Rhino3dmLoader();
  loader.setLibraryPath( 'https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/' );
  loader.load( '/upload/output.3dm', function ( object ) {

    scene.add( object );
    initGUI( object.userData.layers );

    // hide spinner
    document.getElementById( 'loader' ).style.display = 'none';

  } );

  controls = new OrbitControls( camera, renderer.domElement );
  controls.enableZoom = true;
  controls.enableDamping = true;
  controls.dampingDactor = 0.05;

  controls.minDistance = 100;
	controls.maxDistance = 500;

  controls.maxPolarAngle = Math.PI / 2;

  window.addEventListener( 'resize', resize );
function resize() {

  const width = window.innerWidth;
  const height = window.innerHeight;

  camera.aspect = width / height;
  camera.updateProjectionMatrix();

  renderer.setSize( width, height );

}

function animate() {

  controls.update();
  renderer.render( scene, camera );
  renderer.setPixelRatio(2);
  requestAnimationFrame( animate );

}

function initGUI( layers ) {
  gui = new GUI({ 
  title: 'test',
  });

  for ( let i = 0; i < layers.length; i ++ ) {

    const layer = layers[ i ];
    gui.add( layer, 'visible' ).name( layer.name ).onChange( function ( val ) {

      const name = this.object.name;

      scene.traverse( function ( child ) {

        if ( child.userData.hasOwnProperty( 'attributes' ) ) {

          if ( 'layerIndex' in child.userData.attributes ) {

            const layerName = layers[ child.userData.attributes.layerIndex ].name;

            if ( layerName === name ) {

              child.visible = val;
              layer.visible = val;

            }

          }

        }

      } );

    } );

  }

}
</script> '''

components.html(html_string)
st.markdown(html_string, unsafe_allow_html=True) 


1 Like

I dont know if this has been answered, but st.markdown doesnt execute javascript code. I believe there is a custom component that does (I will try to find it and edit this comment).


Edit: So I was thinking of another component, but the component.html should execute javascript fine.

I think you forgot to add the threejs script elements. An old project of mine had the following:

<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.js"></script>

<script type="module">
    import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js';
    import { OrbitControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js';

// rest of code here

</script>

I think this shows two ways to import. You can use cdnjs links in import statement or separate script elements. Basically, there has to be a way to find/get the javascript modules you are referencing


One of the custom components I built has the ability to execute javascript, but its for html presentation slides.

If you didnt already find a solution and still need a component that allows you to execute threeJS code and display result, I could build one pretty easily using code from the aforementioned component.

I dont know how much demand there is for something like this. So let me know.

Hi @bouzidanas,

Thanks for the reply, I tried components, but it didn’t work. I don’t think I am missing any imports because I used this same code in my flask application and it worked fine.

This is what I tried.

import streamlit as st
import streamlit.components.v1 as components

components.html('''
<h1>HTML</h1>

<div id="viewer"></div>

<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.js"></script>

<script type="module">
    import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js';
    import { OrbitControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js';

    import { Rhino3dmLoader } from 'https://cdn.skypack.dev/three/examples/jsm/loaders/3DMLoader.js';
    import { GUI } from 'https://cdn.skypack.dev/three/examples/jsm/libs/dat.gui.module.js';

    let camera, scene, renderer;
    let controls, gui;

    init();
    animate();

    function init() {
      renderer = new THREE.WebGLRenderer({ antialias: true });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.getElementById('viewer').appendChild(renderer.domElement);

      camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 10000);
      camera.position.set(40, -40, 50);

      scene = new THREE.Scene();

      const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
      scene.add(ambientLight);

      const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
      directionalLight.position.set(20, 40, 100);
      scene.add(directionalLight);

      const loader = new Rhino3dmLoader();
      loader.setLibraryPath('https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/');
      loader.load('./output.3dm', function (object) {
        scene.add(object);
        initGUI(object.userData.layers);
      });

      controls = new OrbitControls(camera, renderer.domElement);
      controls.enableZoom = true;
      controls.enableDamping = true;
      controls.dampingDactor = 0.05;
      controls.minDistance = 100;
      controls.maxDistance = 500;
      controls.maxPolarAngle = Math.PI / 2;

      window.addEventListener('resize', resize);
    }

    function resize() {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    }

    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }

    function initGUI(layers) {
      gui = new GUI({ title: 'test' });

      for (let i = 0; i < layers.length; i++) {
        const layer = layers[i];
        gui.add(layer, 'visible').name(layer.name).onChange(function (val) {
          const name = this.object.name;
          scene.traverse(function (child) {
            if (child.userData.hasOwnProperty('attributes')) {
              if ('layerIndex' in child.userData.attributes) {
                const layerName = layers[child.userData.attributes.layerIndex].name;
                if (layerName === name) {
                  child.visible = val;
                  layer.visible = val;
                }
              }
            }
          });
        });
      }
    }
</script>
''',
height=600)```

According to the console, something is wrong with the imports you added (response code 500). I just tested an example I have to see if I can get ThreeJS working and it works.

The example:

import streamlit as st
import streamlit.components.v1 as components

components.html('''
<style>
    *
    {
        margin: 0;
        padding: 0;
    }

    html,
    body
    {
        overflow: hidden;
        min-height: 700px;
    }

    .webgl
    {
        position: fixed;
        top: 0;
        left: 0;
        outline: none;
    }
</style>

<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<!--<script src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.js"></script> -->
<!-- <script src="http://threejs.org/examples/js/controls/TrackballControls.js"></script> -->


<script type="module">
    import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js';
    import { OrbitControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js';
    //import { TrackballControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/TrackballControls.js';
      
    // Base
    // ----------
    
    // Initialize scene
    const scene = new THREE.Scene()
    
    // Initialize camera
    const camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.1, 60)
    
    // Reposition camera
    camera.position.set(6, 0, 0)
    
    // Initialize renderer
    const renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: true
    })
    
    // Set renderer size
    renderer.setSize(window.innerWidth, window.innerHeight)
    
    // Append renderer to body
    document.body.appendChild(renderer.domElement)
    
    // Initialize controls
    const controls = new OrbitControls(camera, renderer.domElement)
    
    // World
    // ----------
    
    // Load world texture
    const worldTexture = new THREE.TextureLoader().load("https://assets.codepen.io/141041/small-world.jpg")
    
    // Initialize world geometry
    const worldGeometry = new THREE.SphereGeometry(1, 40, 40)
    
    // Initialize world material
    const worldMaterial = new THREE.MeshLambertMaterial({
      map: worldTexture
    })
    
    // Initialize world
    const world = new THREE.Mesh(worldGeometry, worldMaterial)
    
    // Add earth to scene
    scene.add(world)
    
    // Clouds
    // ----------
    
    // Load clouds texture
    const cloudTexture = new THREE.TextureLoader().load("https://assets.codepen.io/141041/small-world-clouds.png")
    
    // Initialize clouds geometry
    const cloudGeometry = new THREE.SphereGeometry(1.01, 40, 40)
    
    // Initialize clouds material
    const cloudMaterial = new THREE.MeshBasicMaterial({
      map: cloudTexture,
      transparent: true
    })
    
    // Initialize clouds
    const clouds = new THREE.Mesh(cloudGeometry, cloudMaterial)
    
    // Add clouds to scene
    scene.add(clouds)

    // add subtle ambient lighting
    const ambientLight = new THREE.AmbientLight(0xbbbbbb);
    scene.add(ambientLight);

    // directional lighting
    const directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(1, 1, 1).normalize();
    scene.add(directionalLight);
    
    // Animation
    // ----------      
    
    // Prepare animation loop
    function animate() {
      // Request animation frame
      requestAnimationFrame(animate)
      
      // Rotate world
      world.rotation.y += 0.0005
      
      // Rotate clouds
      clouds.rotation.y -= 0.001
      
      // Render scene
      renderer.render(scene, camera)

    }
    
    // Animate
    animate()
    
    // Resize
    // ----------
    
    // Listen for window resizing
    window.addEventListener('resize', () => {
      // Update camera aspect
      camera.aspect = window.innerWidth / window.innerHeight
      
      // Update camera projection matrix
      camera.updateProjectionMatrix()
      
      // Resize renderer
      renderer.setSize(window.innerWidth, window.innerHeight)

    });
</script>

<style>
  body{
    background: radial-gradient(circle at center, white, rgba(113,129,191,0.5) 50%);
  }
</style>
''',
height=600)

see if this works for you so that you can identify if there is something wrong with your setup.

Hi @bouzidanas,

Looks like theres some progress but still not showing up anything.

So I imported the libraries properly. Theres no visible errors so I’m not sure. The 3dm is in the same folder as my .py

import streamlit.components.v1 as components

components.html('''

<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>

<script type="module">
    import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js';
    import { OrbitControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js';
    import { Rhino3dmLoader } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/loaders/3DMLoader.js';
      
    let camera, scene, renderer;
    let controls, gui;
    init();
    animate();

    function init() {
    
      THREE.Object3D.DEFAULT_UP.set( 0, 0, 1 );

      renderer = new THREE.WebGLRenderer( { antialias: true } );
      renderer.setPixelRatio( 2 );
      renderer.setSize( window.innerWidth, window.innerHeight );
      renderer.outputEncoding = THREE.sRGBEncoding;
      document.body.appendChild( renderer.domElement );

      camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 10000 );
      camera.position.set( 40, -40, 50 );

      scene = new THREE.Scene();

      const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
      scene.add(ambientLight);
      
      const directionalLight = new THREE.DirectionalLight( 0xffffff, 2 );
      directionalLight.position.set( 20, 40, 100);
      scene.add( directionalLight );

      const loader = new Rhino3dmLoader();
      loader.setLibraryPath( 'https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/' );
      loader.load( './output.3dm', function ( object ) {

        scene.add( object );
        initGUI( object.userData.layers );

        // hide spinner
        document.getElementById( 'loader' ).style.display = 'none';

      } );

      controls = new OrbitControls( camera, renderer.domElement );
      controls.enableZoom = true;
      controls.enableDamping = true;
      controls.dampingDactor = 0.05;

      controls.minDistance = 100;
      controls.maxDistance = 500;

      controls.maxPolarAngle = Math.PI / 2;

      window.addEventListener( 'resize', resize );

    function resize() {

      const width = window.innerWidth;
      const height = window.innerHeight;

      camera.aspect = width / height;
      camera.updateProjectionMatrix();

      renderer.setSize( width, height );

    }

    function animate() {

      controls.update();
      renderer.render( scene, camera );
      renderer.setPixelRatio(2)
      requestAnimationFrame( animate );

    }

    }

</script>

''',
height=600)

Thanks

I think there are multiple issues now. Change,

THREE.Object3D.DEFAULT_UP.set( 0, 0, 1 );

to

THREE.Object3D.DefaultUp.set( 0, 0, 1 );

and then the next issue is that output.3dm is not found

you might have to straight up include the contents or figure out where to put it so that the component.html can find it

Yea I’m not sure how to include it straight up because I don’t think it’s possible, is there another way to reference the file?

I think this comment might provide a solution. Alternatively, you can put the file somewhere in the cloud where you can access it by url and that might be a better solution.

Thanks for the help @bouzidanas

I might just skip this and not pursue this issue further as streamlit currently just lacks the capabilities, but hopefully JS becomes more easily usable within streamlit

Thanks for all your help.

Hi fren,

I believe the issue you’re facing is that the JavaScript code within the HTML string is not executed properly within the Streamlit app. Streamlit’s components.html function does not automatically execute JavaScript code. To embed a 3D viewer built with Three.js in a Streamlit app, you can follow these steps:

  1. Move the JavaScript code to a separate JavaScript file (e.g., viewer.js). Remove the <script> tags from the HTML string and leave only the contents of the <script> tag.
// viewer.js

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { Rhino3dmLoader } from 'three/addons/loaders/3DMLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

let camera, scene, renderer;
let controls, gui;

function init() {
  // Remaining code...
}

function animate() {
  // Remaining code...
}

function initGUI(layers) {
  // Remaining code...
}

init();
animate();

  1. Place the JavaScript code in a separate file (viewer.js) in the same directory as your Streamlit app script.
  2. Modify your Streamlit app script to include the viewer.js file using the components.html function.
import streamlit as st
import streamlit.components.v1 as components

html_string = '''
<h1>HTML</h1>
'''

components.html(html_string)

# Load and embed the JavaScript file
with open("viewer.js", "r") as js_file:
    js_code = js_file.read()
components.html(js_code)

Ensure the viewer.js file is in the same directory as your Streamlit app script. When the Streamlit app runs, it will embed the viewer.js file using the components.html function, and the JavaScript code will be executed correctly, rendering the 3D viewer.
I hope it helps you to find out the solution, if there is anything in which I can help you can reach out to me.

Hi @ShivamAgarwal-code,

I tried doing this however it seems like the output is just text of the js code on the streamlit website.

Thanks

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.