PyScript, an innovative framework, bridges the gap between Python’s simplicity and the vast capabilities of web technologies. Leveraging WebAssembly and Pyodide, PyScript offers an intriguing approach to web application development, particularly for complex tasks like 3D rendering using WebGL. This article explores the fascinating world of 3D graphics in the browser, utilizing the PyScript framework and WebGL capabilities through a hands-on example.

What is WebGL and Its Significance in Python-powered Web Development?

WebGL (Web Graphics Library) is a JavaScript API for rendering high-performance interactive 3D and 2D graphics within any compatible web browser without the use of plug-ins. By combining Python’s accessibility with WebGL’s rendering power, developers can create sophisticated 3D visualizations directly in the browser.

Introducing the webgl.html Example

Our webgl.html example demonstrates how Python, through PyScript, can be used to create stunning 3D graphics using WebGL. The example showcases a dynamic 3D scene with moving particles and cubes, all rendered in real-time within the browser. It’s an excellent illustration of how Python’s syntax and libraries, combined with WebGL’s graphical prowess, can result in visually appealing and interactive web applications.

The HTML Setup for 3D Graphics:

<html lang="en">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.147.0/three.min.js"></script>

        <script defer src="https://pyscript.net/latest/pyscript.js"></script>
        <link
            rel="stylesheet"
            href="https://pyscript.net/latest/pyscript.css"
        />
        <py-script>
            from pyodide.ffi import create_proxy, to_js
            from js import window
            from js import Math
            from js import THREE
            from js import performance
            from js import Object
            from js import document
            import asyncio

            mouse = THREE.Vector2.new();

            renderer = THREE.WebGLRenderer.new({"antialias":True})
            renderer.setSize(1000, 1000)
            renderer.shadowMap.enabled = False
            renderer.shadowMap.type = THREE.PCFSoftShadowMap
            renderer.shadowMap.needsUpdate = True

            document.body.appendChild( renderer.domElement )

            import js, pyodide
            def onMouseMove(event):
              event.preventDefault();
              mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
              mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
            js.document.addEventListener('mousemove', pyodide.ffi.create_proxy(onMouseMove))

            camera = THREE.PerspectiveCamera.new( 35, window.innerWidth / window.innerHeight, 1, 500 )
            scene = THREE.Scene.new()
            cameraRange = 3

            camera.aspect = window.innerWidth / window.innerHeight
            camera.updateProjectionMatrix()
            renderer.setSize( window.innerWidth, window.innerHeight )

            setcolor = "#000000"

            scene.background = THREE.Color.new(setcolor)
            scene.fog = THREE.Fog.new(setcolor, 2.5, 3.5);

            sceneGroup = THREE.Object3D.new();
            particularGroup = THREE.Object3D.new();

            def mathRandom(num = 1):
              setNumber = - Math.random() * num + Math.random() * num
              return setNumber

            particularGroup =  THREE.Object3D.new();
            modularGroup =  THREE.Object3D.new();

            perms = {"flatShading":True, "color":"#111111", "transparent":False, "opacity":1, "wireframe":False}
            perms = Object.fromEntries(to_js(perms))

            particle_perms = {"color":"#FFFFFF", "side":THREE.DoubleSide}
            particle_perms = Object.fromEntries(to_js(particle_perms))

            def create_cubes(mathRandom, modularGroup):
              i = 0
              while i < 30:
                geometry = THREE.IcosahedronGeometry.new();
                material = THREE.MeshStandardMaterial.new(perms);
                cube = THREE.Mesh.new(geometry, material);
                cube.speedRotation = Math.random() * 0.1;
                cube.positionX = mathRandom();
                cube.positionY = mathRandom();
                cube.positionZ = mathRandom();
                cube.castShadow = True;
                cube.receiveShadow = True;
                newScaleValue = mathRandom(0.3);
                cube.scale.set(newScaleValue,newScaleValue,newScaleValue);
                cube.rotation.x = mathRandom(180 * Math.PI / 180);
                cube.rotation.y = mathRandom(180 * Math.PI / 180);
                cube.rotation.z = mathRandom(180 * Math.PI / 180);
                cube.position.set(cube.positionX, cube.positionY, cube.positionZ);
                modularGroup.add(cube);
                i += 1

            create_cubes(mathRandom, modularGroup)


            def generateParticle(mathRandom, particularGroup, num, amp = 2):
              gmaterial = THREE.MeshPhysicalMaterial.new(particle_perms);
              gparticular = THREE.CircleGeometry.new(0.2,5);
              i = 0
              while i < num:
                pscale = 0.001+Math.abs(mathRandom(0.03));
                particular = THREE.Mesh.new(gparticular, gmaterial);
                particular.position.set(mathRandom(amp),mathRandom(amp),mathRandom(amp));
                particular.rotation.set(mathRandom(),mathRandom(),mathRandom());
                particular.scale.set(pscale,pscale,pscale);
                particular.speedValue = mathRandom(1);
                particularGroup.add(particular);
                i += 1

            generateParticle(mathRandom, particularGroup, 200, 2)

            sceneGroup.add(particularGroup);
            scene.add(modularGroup);
            scene.add(sceneGroup);

            camera.position.set(0, 0, cameraRange);
            cameraValue = False;

            ambientLight = THREE.AmbientLight.new(0xFFFFFF, 0.1);

            light = THREE.SpotLight.new(0xFFFFFF, 3);
            light.position.set(5, 5, 2);
            light.castShadow = True;
            light.shadow.mapSize.width = 10000;
            light.shadow.mapSize.height = light.shadow.mapSize.width;
            light.penumbra = 0.5;

            lightBack = THREE.PointLight.new(0x0FFFFF, 1);
            lightBack.position.set(0, -3, -1);

            scene.add(sceneGroup);
            scene.add(light);
            scene.add(lightBack);

            rectSize = 2
            intensity = 14
            rectLight = THREE.RectAreaLight.new( 0x0FFFFF, intensity,  rectSize, rectSize )
            rectLight.position.set( 0, 0, 1 )
            rectLight.lookAt( 0, 0, 0 )
            scene.add( rectLight )

            raycaster = THREE.Raycaster.new();
            uSpeed = 0.1

            time = 0.0003;
            camera.lookAt(scene.position)

            async def main():
              while True:
                time = performance.now() * 0.0003;
                i = 0
                while i < particularGroup.children.length:
                  newObject = particularGroup.children[i];
                  newObject.rotation.x += newObject.speedValue/10;
                  newObject.rotation.y += newObject.speedValue/10;
                  newObject.rotation.z += newObject.speedValue/10;
                  i += 1

                i = 0
                while i < modularGroup.children.length:
                  newCubes = modularGroup.children[i];
                  newCubes.rotation.x += 0.008;
                  newCubes.rotation.y += 0.005;
                  newCubes.rotation.z += 0.003;

                  newCubes.position.x = Math.sin(time * newCubes.positionZ) * newCubes.positionY;
                  newCubes.position.y = Math.cos(time * newCubes.positionX) * newCubes.positionZ;
                  newCubes.position.z = Math.sin(time * newCubes.positionY) * newCubes.positionX;
                  i += 1

                particularGroup.rotation.y += 0.005;

                modularGroup.rotation.y -= ((mouse.x * 4) + modularGroup.rotation.y) * uSpeed;
                modularGroup.rotation.x -= ((-mouse.y * 4) + modularGroup.rotation.x) * uSpeed;

                renderer.render( scene, camera )
                await asyncio.sleep(0.02)

            asyncio.ensure_future(main())
        </py-script>
</html>

Key Components of the webgl.html Python Script:

  1. Scene and Renderer Setup: Utilizes THREE.js for setting up the WebGL renderer and creating the 3D scene.
  2. Creating 3D Objects: Constructs icosahedron geometries (cubes) with random positions, rotations, and scales.
  3. Animating Particles: Generates a group of particles with dynamic behavior, adding life to the scene.
  4. Interactive Camera Controls: Adjusts the camera’s view based on mouse movements for interactive user experience.
  5. Dynamic Lighting and Shadows: Implements lighting and shadow effects to enhance visual depth.
  6. Continuous Rendering Loop: Uses an asynchronous loop for updating object positions and rendering the scene.

Enhancing Your Web Applications with 3D Graphics

The integration of PyScript and WebGL opens up new possibilities for web application development. With Python’s straightforward syntax and the graphical capabilities of WebGL, developers can craft interactive and visually striking web applications. This combination is particularly beneficial for educational tools, data visualization, and interactive storytelling.

Conclusion and Next Steps

PyScript’s ability to harness the power of Python in the browser, combined with WebGL’s 3D rendering capabilities, marks a significant step forward in web development. Whether you’re a seasoned developer or a Python enthusiast venturing into web development, the potential of PyScript and WebGL is worth exploring.

For a hands-on experience, dive into the webgl.html example included below:

WebGL Popup