In this post I will share how I was able to add ThreeJS to block plugins in WordPress.
First I needed to bundle my own library of ThreeJS using Webpack. You can follow the post below in the steps I took to create the custom bundle.
Then I created a single page with a custom OBJ and MTL . Follow this link below for my steps.
Adding ThreeJS To WordPress Block Plugins
Code
A little note about the interface when testing in the editor. While testing in the editor, the OBJ has to be reloaded to update the light intensity changes form the slider. This is due to the way the editor is created with React useEffect. It is a minor detail that wasn't worth chasing as long as its working properly on the frontend.
Edit Block.json
We will edit the block.json file here. I didn't add anything special here, just an attribute for background-color. The renderer for the ThreeJS scene is set to transparent so the color will show through.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "r2creations24/my-custom-blocks-threejs",
"version": "0.1.0",
"title": "My Custom Blocks ThreeJS",
"category": "widgets",
"icon": "smiley",
"description": "Custom blocks created by R2creations24.",
"example": {},
"attributes": {
"bgColor": {
"type": "string",
"default": "#219b8b"
}
},
"supports": {
"html": false
},
"textdomain": "my-custom-blocks-threejs",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"render": "file:./render.php",
"style": "file:./style-index.css",
"viewScript": "file:./view.js"
}
Update the Stylesheet Used For Frontend and Backend
Next we will edit the style.scss file so the same stylesheet is used for both the frontend and the backend.
.wp-block-r2creations24-my-custom-blocks-threejs {
background-color: var(--bg-color);
color: #fff;
padding: 2px;
display: flex;
margin: auto;
align-items: center;
}
.wp-block-r2creations24-my-custom-blocks-threejs .threejs_example {
width: 90%;
margin: auto;
display: grid;
align-content: center;
margin: auto;
}
.wp-block-r2creations24-my-custom-blocks-threejs .threejs_example > canvas {
display: block;
margin: auto;
}
Edit the Plugins Root PHP File
Now we need to add the custom ThreeJS bundle script to the plugins root PHP file. This file will be located at the root of the plugin directory named as your plugin name. Adding the script here will load it at the frontend so no need to add the script inside the render.php file. Add the following at the bottom of the file.
function r2creations24_my_custom_blocks_enqueue_script()
{
wp_enqueue_script( 'r2creations24_my_custom_blocks_script_custom_threejs', plugin_dir_url( __FILE__ ) . 'assets/scripts/custom_threejs.js' );
}
add_action('wp_enqueue_scripts', 'r2creations24_my_custom_blocks_enqueue_script');
Edit The Save.JS To Return Null
export default function save() {
return null;
}
Edit The Render.php File For Frontend
First add the following to the top of the render.php file. This will load the DOM elements and load the attributes set in the block plugin to use as CSS variables in the stylesheet.
<?php
$bgColor = $attributes['bgColor'];
$style = "--bg-color: ".$bgColor.";";
?>
<div <?php echo get_block_wrapper_attributes(array("style" => $style)); ?>>
<div class="threejs_example">
<input type="range" default="20" min="0" max="100" step="1" onchange="changeLight(this.value)"/>
<input type="button" value="Change Color Tint" onclick="changeColorTint()">
<canvas id="threejs_canvas" width="450px" height="450px" ></canvas>
</div>
</div>
<script>
//add the script code here
</script>
Next we will add the code inside the script tag.
Breakdown For The Script Tag
Start with creating variables as well as the call to the init() function.
//Threejs scene
let camera, scene, renderer, pointLight;
//object
let object, material;
//parent DIV
let canvasParent;
//PointLight intensity
let lightInt = 20;
init();
The Init() Function Breakdown
Below I will breakdown the contents of the init() function.
function init() {
//add the rest of the init code here
function onProgress( xhr ) {
if ( xhr.lengthComputable ) {
const percentComplete = xhr.loaded / xhr.total * 100;
console.log( 'model ' + percentComplete.toFixed( 2 ) + '% downloaded' );
}
}
function onError() {}
}
We will use the canvasParent for canvas sizing.
canvasParent = document.getElementsByClassName("threejs_example")[0];
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 200 );
camera.aspect = canvasParent.clientWidth / canvasParent.clientWidth;
camera.updateProjectionMatrix();
camera.position.z = 6;
scene = new THREE.Scene();
const ambientLight = new THREE.AmbientLight( 0xffffff );
scene.add( ambientLight );
const pointLight = new THREE.PointLight( 0xffffff, lightInt );
camera.add( pointLight );
scene.add( camera );
Next we will load the model into the scene.
const manager = new THREE.LoadingManager( loadModel );
function loadModel() {
object.traverse( function ( child ) {
if ( child.isMesh ) {
console.log(material);
child.material = material;
child.material.shininess = 0.0;
}
} );
object.rotation.y = .55;
object.rotation.x = .55;
scene.add( object );
animate();
}
We are creating the model and material from an asset exported from Blender.
const materialLoader = new MTLLoader()
.setPath('<?php echo plugins_url(); ?>/my-custom-blocks/assets/models/')
.load( 'cube.mtl', function ( materials ) {
materials.preload();
material = materials.getAsArray()[0];
} );
const loader = new OBJLoader( manager );
loader.load( '<?php echo plugins_url(); ?>/my-custom-blocks/assets/models/cube.obj', function ( obj ) {
object = obj;
}, onProgress, onError );
Here we are referencing the DOM canvas to attach to the ThreeJS renderer.
const useCanvas = document.getElementById("threejs_canvas");
renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true, canvas: useCanvas } );
renderer.setSize( canvasParent.clientWidth * .50, canvasParent.clientWidth * .50);
const controls = new OrbitControls( camera, renderer.domElement );
controls.minDistance = 4;
controls.maxDistance = 15;
controls.addEventListener( 'change', render );
window.addEventListener( 'resize', onWindowResize );
Rest of Functions After Init()
Following the init() function, we have a few more functions to add. Add these outside of the init() function.
function onWindowResize() {
camera.aspect = canvasParent.clientWidth / canvasParent.clientWidth;
camera.updateProjectionMatrix();
renderer.setSize( canvasParent.clientWidth * .50, canvasParent.clientWidth * .50 );
}
These functions are used to change the object color when the button is clicked.
function changeColorTint(){
object.traverse(function (child) {
if (child.isMesh) {
child.material.color.set(randomColor());
}
});
}
function randomColor(){
return Math.floor(Math.random() * 0xffffff);
}
This function is used to change the PointLight intensity when the slider is changed.
function changeLight(value){
lightInt = value;
if(pointLight !== null)
camera.remove( pointLight );
pointLight = new THREE.PointLight( 0xffffff, lightInt );
camera.add( pointLight );
}
Finally we need to create the animation function and add the render function.
function render() {
renderer.render( scene, camera );
}
function animate(){
requestAnimationFrame(animate);
object.rotation.y +=.002;
object.rotation.x +=.001;
renderer.setClearColor(0xffffff, 0);
render();
}
Edit The Editor With Edit.JS
First we need to import libraries we will use and set some basic variables.
import { __ } from '@wordpress/i18n';
import {
useBlockProps,
PanelColorSettings,
InspectorControls
} from '@wordpress/block-editor';
import {
PanelBody
} from '@wordpress/components';
import './editor.scss';
import { useRef, useEffect, useState } from '@wordpress/element';
import './editor.scss';
import '../../assets/scripts/custom_threejs.js';
export default function Edit( { attributes, setAttributes } ) {
const newStyle = {
"--bg-color": attributes.bgColor
};
const blockProps = useBlockProps( { style: newStyle } );
const [ lightInt, setlightInt ] = useState( 20 );
let pointLight, camera, object;
const ref = useRef();
const parentref = useRef();
//rest of code here
}
This function is used to set the background color for the block plugin.
function setBgColor(value){
if(value === undefined){
setAttributes({bgColor: "#219b8b"});
} else {
setAttributes({bgColor: value});
}
}
This function changes the PointLight intensity.
function changeLight(value){
setlightInt(value);
}
Changes the objects material color with a random color.
function changeColorTint(){
object.traverse(function (child) {
if (child.isMesh) {
// Access the material of the child mesh here
child.material.color.set(randomColor());
}
});
}
function randomColor(){
return Math.floor(Math.random() * 0xffffff);
}
useEffect To Ensure Loading After Rest Of DOM Is Done
Next we need to add in the ThreeJS code into the useEffect function to ensure it starts loading after the rest of the DOM is done loading. We included a dependency of the lightInt. This means when the lightInt variable changes, the useEffect will be called to run again.
useEffect(() => {
//add the next few items in here
}, [lightInt]);
Start with creating some variables.
let scene, renderer;
let material;
init();
Breakdown of init() Function Contents
This is almost the same as the render.php init() function but different enough I figured I would post it. The editor uses React references to find the element.
function init() {
//add the next few blocks in here
function onProgress( xhr ) {
if ( xhr.lengthComputable ) {
const percentComplete = xhr.loaded / xhr.total * 100;
console.log( 'model ' + percentComplete.toFixed( 2 ) + '% downloaded' );
}
}
function onError() {}
}
camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.1, 200 );
camera.aspect = parentref.current.clientWidth / parentref.current.clientWidth;
camera.updateProjectionMatrix();
camera.position.z = 6;
scene = new THREE.Scene();
const ambientLight = new THREE.AmbientLight( 0xffffff );
scene.add( ambientLight );
pointLight = new THREE.PointLight( 0xffffff, lightInt );
camera.add( pointLight );
scene.add( camera );
const manager = new THREE.LoadingManager( loadModel );
function loadModel() {
object.traverse( function ( child ) {
if ( child.isMesh ) {
console.log(material);
child.material = material;
child.material.shininess = 0.0;
}
} );
object.rotation.y = .55;
object.rotation.x = .55;
scene.add( object );
animate();
}
const materialLoader = new MTLLoader()
.setPath('../wp-content/plugins/my-custom-blocks/assets/models/')
.load( 'cube.mtl', function ( materials ) {
materials.preload();
material = materials.getAsArray()[0];
} );
const loader = new OBJLoader( manager );
loader.load( '../wp-content/plugins/my-custom-blocks/assets/models/cube.obj', function ( obj ) {
object = obj;
}, onProgress, onError );
renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true, canvas: ref.current } );
renderer.setSize( parentref.current.clientWidth * .50, parentref.current.clientWidth * .50);
const controls = new OrbitControls( camera, renderer.domElement );
controls.minDistance = 4;
controls.maxDistance = 15;
controls.addEventListener( 'change', render );
window.addEventListener( 'resize', onWindowResize );
Other Necessary Functions Inside UseEffect But Out Of init() Function
function onWindowResize() {
camera.aspect = parentref.current.clientWidth / parentref.current.clientWidth;
camera.updateProjectionMatrix();
renderer.setSize( parentref.current.clientWidth * .50, parentref.current.clientWidth * .50 );
}
function render() {
renderer.render( scene, camera );
}
function animate(){
requestAnimationFrame(animate);
object.rotation.y +=.002;
object.rotation.x +=.001;
renderer.setClearColor(0xffffff, 0);
render();
}
Return Editor Panel And Page Element
return (
<>
// add the below InspectorControls here
//add the below HTML DIV blocks here
</>
);
}
<InspectorControls>
<PanelBody title={ __( 'Settings', 'my-custom-blocks-threejs' ) }>
<PanelColorSettings
title = { __('Configure Color')}
colorSettings={
[
{
value: attributes.bgColor,
onChange: (value) => setBgColor(value),
label: __('Background Color')
}
]
}
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps } ref={parentref}>
<div class="threejs_example">
<input type="range" min="0" max="150" value={lightInt} onChange={(event) => changeLight(event.target.value)}/>
<input type="button" value="Change Color Tint" onClick={changeColorTint}></input>
<canvas ref={ref} width="450px" height="450px" ></canvas>
</div>
</div>
Hope this was useful.